Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Node.js Backend Developer Top 100 Interview Questions

100 Node.js backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the event loop, async patterns, streams, HTTP servers, databases, caching, testing, security, performance and system design, from the basics to production scenarios.

100 questions

Questions 21-40 of 100

  1. Q21.What is prototype pollution, and how do you prevent it?

    Prototype pollution happens when attacker-controlled keys such as __proto__ or constructor.prototype let input modify Object.prototype. Every object in the process then appears to have the injected property, which can bypass checks such as if (user.isAdmin).

    How it usually happens:

    • Deep-merge, clone or "set by path" helpers that copy user-supplied JSON into existing objects key by key.
    • Query string parsers that build nested objects from keys like a[__proto__][x].

    How to prevent it:

    1. Skip dangerous keys in any recursive merge or path assignment: __proto__, constructor and prototype.
    2. Validate input with a schema that allows only known keys, so unexpected keys never reach merge code.
    3. Use prototype-free objects: Object.create(null) or a Map for dictionaries keyed by user data.
    4. Check own properties with Object.hasOwn instead of in or plain property access when the property controls security.
    5. Keep libraries patched, since many merge helpers have had pollution vulnerabilities.

    Some teams also freeze Object.prototype at startup as defense in depth, after testing that their dependencies tolerate it.

    Example

    A naive merge lets a parsed payload add isAdmin to every object, while the safe merge skips the dangerous key.

    const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
    
    function merge(target, source, { safe }) {
      for (const key of Object.keys(source)) {
        if (safe && BLOCKED.has(key)) continue;
        const value = source[key];
        if (typeof value === 'object' && value !== null) {
          target[key] = merge(target[key] || {}, value, { safe });
        } else {
          target[key] = value;
        }
      }
      return target;
    }
    
    const payload = JSON.parse('{"profile": {"name": "Asha"}, "__proto__": {"isAdmin": true}}');
    
    merge({}, payload, { safe: true });
    console.log({}.isAdmin); // undefined
    
    merge({}, payload, { safe: false });
    console.log({}.isAdmin); // true
    delete Object.prototype.isAdmin;
  2. Q22.How do you handle numbers, large integers and money safely in JavaScript?

    JavaScript's number type is a 64-bit floating point value. It is exact for integers only up to Number.MAX_SAFE_INTEGER and cannot represent most decimal fractions exactly, which matters for identifiers and money.

    • Decimal fractions: 0.1 + 0.2 is not exactly 0.3, so never store or compare money as floating point amounts.
    • Money: store integer minor units, such as paise or cents, do arithmetic on integers, and format only for display. Use a decimal library when rates and rounding rules get complex.
    • Large integers: identifiers above about nine quadrillion silently lose precision, including when parsed from JSON. Transmit them as strings, or use BigInt.
    • BigInt: exact integers of any size, but it cannot mix with number in arithmetic and JSON.stringify rejects it without a custom conversion.
    • Parsing: Number('') is 0 and parseInt('12px') is 12, so validate input strictly instead of trusting either.

    Display: Intl.NumberFormat formats currencies and locales correctly, including grouping digits and currency symbols.

    Example

    Floating point drift, precision loss on large IDs and exact integer money arithmetic, side by side.

    console.log(0.1 + 0.2); // 0.30000000000000004
    console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
    console.log(JSON.parse('{"id": 9007199254740993}').id); // 9007199254740992
    console.log((9007199254740993n + 1n).toString()); // 9007199254740994
    
    const priceInPaise = 1999;
    const totalInPaise = priceInPaise * 3;
    console.log((totalInPaise / 100).toFixed(2)); // 59.97
  3. Q23.How do async iterators and `for await` help process data in Node.js?

    An async iterator produces values one at a time, where each value may arrive later. for await...of consumes them in order, waiting for each one, which makes paginated APIs, streams and queues read like simple loops.

    • Async generators: an async function* can await inside and yield each result, such as fetching one page of an external API at a time.
    • Streams are async iterable, so for await (const chunk of request) reads a request body without event handlers.
    • Natural backpressure: the producer only does work when the loop asks for the next value.
    • Early exit: break or an error calls the iterator's return method, so a well-written generator stops fetching and closes resources.

    Things to watch:

    • Items are processed sequentially; for concurrency, process batches or use a concurrency-limited pool.
    • Put cleanup in a finally block inside the generator, so it runs on early exit too.
    • An error thrown inside the loop body does not automatically retry; handle it per item if partial failure is acceptable.

    Example

    The loop stops after four jobs, so the generator never fetches pages three to five.

    let pagesFetched = 0;
    
    async function* fetchPages(totalPages) {
      for (let page = 1; page <= totalPages; page++) {
        await new Promise((resolve) => setTimeout(resolve, 5));
        pagesFetched++;
        yield [`job-${page}a`, `job-${page}b`];
      }
    }
    
    async function main() {
      const seen = [];
      for await (const jobs of fetchPages(5)) {
        seen.push(...jobs);
        if (seen.length >= 4) break;
      }
      console.log(seen.join(',')); // job-1a,job-1b,job-2a,job-2b
      console.log(pagesFetched); // 2
    }
    
    main();
  4. Q24.How does garbage collection work in the JavaScript engine that Node.js uses?

    The engine frees memory automatically by finding objects no longer reachable from any root, such as global variables, the stack and active closures. It uses generational collection, because most objects die young.

    • Young generation: new objects are allocated here and collected often with a fast copying collection; survivors are promoted.
    • Old generation: long-lived objects are collected less often with a mark, sweep and compact process, much of it done incrementally or concurrently to keep pauses short.
    • Heap limit: the old generation has a maximum size; when a process reaches it, Node.js crashes with a heap out-of-memory error. --max-old-space-size sets that limit in megabytes.
    • External memory: Buffers and native resources live outside the JavaScript heap but are still tied to objects the collector tracks.

    What this means for services:

    • Allocation rate matters: creating many temporary objects per request causes frequent collections and higher CPU and latency.
    • Leaks are reachability bugs: an object referenced from a global map, a closure or a listener is never collected.
    • GC pauses show up as periodic latency spikes, which you can measure through performance hooks.
  5. Q25.How would you group and aggregate records efficiently in JavaScript?

    Walk the records once and accumulate results in a Map keyed by the group, then sort or slice the much smaller summary. One pass with a Map is linear time, and it avoids repeated filter calls that rescan the array for every group.

    • Why Map: keys can be any type, iteration follows insertion order, and there is no prototype that user-supplied keys could collide with.
    • Nullish coalescing: map.get(key) ?? 0 gives a clean default for the first record of a group.
    • Object.groupBy and Map.groupBy, available in recent versions, return the grouped records themselves when you need lists rather than totals.

    For larger data:

    • If records come from a database, aggregate there with GROUP BY, which avoids moving every row into the process.
    • For streams, update the Map as each record arrives, so memory grows with the number of groups, not records.
    • Keep money totals in integer minor units to avoid floating point drift.

    Example

    Orders are totalled per customer in one pass, the top two customers are selected, and Object.groupBy returns grouped records.

    const orders = [
      { customer: 'asha', total: 250 },
      { customer: 'ravi', total: 100 },
      { customer: 'asha', total: 175 },
      { customer: 'meera', total: 300 },
      { customer: 'ravi', total: 40 },
    ];
    
    const totals = new Map();
    for (const { customer, total } of orders) {
      totals.set(customer, (totals.get(customer) ?? 0) + total);
    }
    
    const top = [...totals].sort((a, b) => b[1] - a[1]).slice(0, 2);
    console.log(top.map(([name, sum]) => `${name}=${sum}`).join(',')); // asha=425,meera=300
    console.log(Object.groupBy(orders, (order) => order.customer).ravi.length); // 2
  6. Q26.What is the difference between concurrency and parallelism in Node.js?

    Concurrency means making progress on many tasks over the same period by switching between them; parallelism means running several tasks at the very same instant on different cores. A single Node.js process is highly concurrent but runs JavaScript on only one core.

    ConcurrencyParallelism
    Many tasks make progress over the same period by switchingSeveral tasks run at the same instant on different cores
    One process handles thousands of requests waiting on I/ONeeds worker threads, several processes or several replicas
    Enough for services that mostly waitNeeded for services that mostly compute
    • Concurrency in Node.js: while one request waits on a database, the event loop runs code for others. Thousands of requests can be in progress, but only one JavaScript callback executes at any moment.
    • Hidden parallelism: network I/O happens in the operating system, and thread-pool work such as file reads and hashing runs on other cores, so some work does happen in parallel behind the scenes.
    • Explicit parallelism: worker threads, multiple processes or several container replicas run JavaScript on multiple cores at once.

    Why the distinction matters:

    • A service that spends its time waiting scales well on one process, because concurrency is enough.
    • A service that spends its time computing needs parallelism, or every request waits behind the one doing the computation.
    • Race conditions still exist in Node.js: two concurrent requests can read and update the same database row between awaits, even though their JavaScript never runs at the same instant.
  7. Q27.How does a Node.js service use multiple CPU cores?

    Because one process runs JavaScript on a single thread, a service uses more cores by running more processes or by moving CPU-heavy functions to worker threads. Each option fits a different need.

    Multiple processesWorker threadsChild processes
    Several copies of the whole server, through the cluster module or container replicasExtra threads in one process, each with its own JavaScript engine instanceSeparate programs started and controlled by the parent
    Spread incoming requests across all coresOffload CPU-heavy functions without blocking the main loopRun external tools or isolate risky work
    Share nothing; coordinate through databases, caches or messagesPass messages, transfer buffers or share memory deliberatelyCommunicate through pipes, arguments and exit codes

    When to use which:

    • Use multiple processes as the default way to scale request handling; in containers, running one process per container and adding replicas is often simplest.
    • Use worker threads for occasional heavy computation, such as generating a report, so the request loop stays responsive.
    • Use child processes to run separate programs or code you want isolated from the server's memory.

    Each process or worker has its own memory and in-process caches, so per-process state is never shared automatically.

  8. Q28.When should you use worker threads in Node.js, and how do they communicate?

    Use worker threads for CPU-bound JavaScript that would otherwise block the event loop, such as parsing huge files, compressing images in pure JavaScript, or heavy calculations. They are not useful for I/O, which Node.js already handles without blocking.

    How they work:

    • Each worker has its own event loop and JavaScript heap, so ordinary objects are not shared.
    • Messages: postMessage sends data, which is copied using the structured clone algorithm.
    • Transfer: large ArrayBuffers can be transferred instead of copied, moving ownership to the worker.
    • Shared memory: SharedArrayBuffer with Atomics lets threads share memory, at the cost of careful synchronization.

    Good practice:

    1. Use a pool: starting a worker takes time and memory, so keep a fixed pool and queue tasks to it, rather than creating one per request.
    2. Keep messages small: copying large objects can cost more than the work itself.
    3. Handle errors and exits: listen for error and exit, and replace crashed workers.
    4. Bound the queue: reject or delay work when the pool is saturated.

    Example

    Two workers compute sums in parallel while the main thread stays free; the results match the closed-form formula.

    const { Worker } = require('node:worker_threads');
    
    const workerCode = `
    const { parentPort, workerData } = require('node:worker_threads');
    let sum = 0;
    for (let i = 1; i <= workerData.n; i++) sum += i * i;
    parentPort.postMessage(sum);
    `;
    
    function sumOfSquares(n) {
      return new Promise((resolve, reject) => {
        const worker = new Worker(workerCode, { eval: true, workerData: { n } });
        worker.once('message', resolve);
        worker.once('error', reject);
      });
    }
    
    Promise.all([sumOfSquares(1000), sumOfSquares(2000)]).then(([first, second]) => {
      console.log(first); // 333833500
      console.log(second); // 2668667000
    });
  9. Q29.How do you limit the concurrency of asynchronous operations in Node.js?

    Run a fixed number of "worker" loops that each take the next item from a shared list until none are left. At most that many operations are in flight, and the list drains as fast as the limit allows.

    Why limits matter:

    • Starting 10,000 HTTP calls at once with Promise.all exhausts sockets and memory, and the downstream service may throttle or fail.
    • Database pools have a fixed size; extra queries just wait inside the pool and time out.
    • Rate-limited APIs reject bursts.

    Design points:

    • Preserve order: store each result at its original index, so the output matches the input order.
    • Error policy: decide whether one failure stops everything or is recorded per item.
    • Size the limit from the dependency's capacity, such as the pool size or the provider's rate limit.
    • Combine with timeouts, so a hung call does not hold a slot forever.

    For continuous streams of work, the same idea becomes a queue with a fixed number of consumers. Across several processes, a per-process limit multiplies by the process count, so a shared limiter is needed for a strict global cap.

    Example

    Seven fetches run with at most three in flight, and the results keep their input order.

    async function mapWithLimit(items, limit, fn) {
      const results = new Array(items.length);
      let next = 0;
      async function worker() {
        while (next < items.length) {
          const index = next++;
          results[index] = await fn(items[index], index);
        }
      }
      const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
      await Promise.all(workers);
      return results;
    }
    
    let running = 0;
    let peak = 0;
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function fetchItem(id) {
      running++;
      peak = Math.max(peak, running);
      await sleep(10);
      running--;
      return id * 10;
    }
    
    mapWithLimit([1, 2, 3, 4, 5, 6, 7], 3, fetchItem).then((results) => {
      console.log(results.join(',')); // 10,20,30,40,50,60,70
      console.log(peak); // 3
    });
  10. Q30.How do timeouts and cancellation work with `AbortController` in Node.js?

    An AbortController creates an AbortSignal that you pass to cancellable operations. Calling abort() makes every operation listening to that signal stop and reject, so one signal can cancel a whole group of work.

    • Built-in support: fetch, many fs/promises and stream functions, timers/promises and child processes accept a signal option.
    • Deadlines: AbortSignal.timeout(ms) creates a signal that aborts itself after the given time.
    • Combining: AbortSignal.any([...signals]) aborts when any of several signals fires, such as a client disconnect or a deadline.
    • Your own code: check signal.aborted, or listen for the abort event, and stop work when it fires.

    Using it in a service:

    1. Create one controller per incoming request, and abort it if the client disconnects.
    2. Pass a deadline signal to every outgoing call, derived from the request's remaining time budget.
    3. When a timeout fires, return a clear error, such as a 504 status, instead of letting the work continue unobserved.

    Aborting rejects the promise with an AbortError; when a timeout signal caused it, the error's cause is a TimeoutError.

    Example

    A query is cancelled by a deadline signal, and a second one by an explicit abort.

    const { setTimeout: sleep } = require('node:timers/promises');
    
    async function slowQuery(signal) {
      await sleep(5000, undefined, { signal });
      return 'rows';
    }
    
    async function main() {
      try {
        await slowQuery(AbortSignal.timeout(50));
      } catch (error) {
        console.log(error.name); // AbortError
        console.log(error.cause.name); // TimeoutError
      }
    
      const controller = new AbortController();
      const pending = slowQuery(controller.signal).catch((error) => error.name);
      controller.abort();
      console.log(await pending); // AbortError
    }
    
    main();
  11. Q31.How should a Node.js service retry failed calls?

    Retry only failures that are likely temporary, only for operations that are safe to repeat, with exponentially growing waits plus random jitter, and within the request's overall deadline.

    flowchart TD
      A["Call dependency"] --> B{"Succeeded?"}
      B -->|Yes| C["Return result"]
      B -->|No| D{"Temporary error and safe to repeat?"}
      D -->|No| E["Fail fast with the error"]
      D -->|Yes| F{"Attempts and deadline left?"}
      F -->|No| E
      F -->|Yes| G["Wait with backoff and jitter"]
      G --> A
    • Which errors: connection resets, timeouts and responses such as 429, 502, 503 and 504 are good candidates; validation errors and most other 4xx responses are not.
    • Which operations: reads and idempotent writes. Retrying a payment without an idempotency key can charge twice.
    • Backoff: wait roughly 100, 200, 400 and 800 milliseconds between attempts, capped at a maximum.
    • Jitter: randomize each wait, for example between zero and the backoff value, so many clients do not retry in synchronized waves.
    • Retry-After: respect the server's suggested delay when it provides one.
    • Budget: stop when the remaining request time is too short, and cap total attempts, typically three or four.

    Avoid retry storms: retry at one layer only. If the client, a gateway and the service each retry three times, one failing call can become 27 attempts against a dependency that is already struggling.

    Record retries as a metric; a rising retry rate is an early warning of an unhealthy dependency.

  12. Q32.How do you profile CPU usage in a Node.js service?

    Capture a CPU profile while the service is under realistic load, then read it as a flame graph to see which functions consume the main thread. Optimizing without a profile usually targets the wrong code.

    1. Reproduce the load in a test environment, or profile a single production instance carefully.
    2. Record a profile: start Node.js with --cpu-prof to write a profile file on exit, or connect the inspector with --inspect and record from developer tools.
    3. Read the flame graph: wide bars are functions that use a lot of time; follow them from your request handler down to the hot spot.
    4. Check event loop delay at the same time, to confirm the main thread is the bottleneck.
    5. Fix and re-measure with the same load.

    Typical findings:

    • JSON serialization of large responses.
    • Synchronous crypto or compression on the request path.
    • Inefficient loops, such as searching arrays inside loops instead of using a Map.
    • Logging that formats large objects on every request.
    • Regular expressions that backtrack heavily.

    Never leave the inspector port open in production; it allows anyone who can reach it to run code in the process.

  13. Q33.How do you find a memory leak in a Node.js service?

    Confirm the heap really grows without bound, then take heap snapshots at different times and compare them to see which objects keep accumulating and what is holding them.

    1. Confirm the trend: track heapUsed over hours of steady traffic. A sawtooth that returns to the same baseline is normal; a rising baseline is a leak.
    2. Take snapshots: capture a heap snapshot after warm-up, run traffic, then capture another. Writing a snapshot pauses the process and needs memory, so use a test instance or remove the instance from traffic first.
    3. Compare: sort by objects allocated between snapshots, and look at their retainers, the chain of references keeping them alive.
    4. Check the usual suspects: unbounded Maps used as caches, listeners added per request, timers never cleared, closures capturing requests, and global arrays of pending work.
    5. Fix and verify by rerunning the same test.

    Watch the external figure too: Buffers live outside the JavaScript heap and can grow even when the heap looks flat.

    Example

    A service's heap grows by about 40 MB every hour.

    1. Two heap snapshots taken an hour apart show 600,000 new arrays of timestamps.
    2. Their retainers lead to a Map in a rate limiter, keyed by client IP address, holding every request time seen.
    3. Entries are added for each new address but never removed, and crawlers bring thousands of new addresses daily.
    4. Removing entries whose newest timestamp is older than the window, plus a cap on total entries, makes the heap flat at about 180 MB.
  14. Q34.What do the fields of `process.memoryUsage()` mean?

    process.memoryUsage() reports how the process's memory is split between the JavaScript heap and everything else. Reading the right field tells you whether a problem is a JavaScript leak, large buffers or something native.

    rssheapUsedheapTotalexternal
    Total memory the operating system has given the processMemory occupied by live JavaScript objectsMemory the engine has reserved for the JavaScript heapMemory held by Buffers and native objects tied to JavaScript
    Includes the heap, code, stacks and native allocationsRises with allocation and drops after garbage collectionAlways at least as large as the used heapNot limited by the heap size setting
    The figure container memory limits are enforced againstThe best signal for JavaScript object leaksShows how much headroom the engine keepsGrows with file, network, compression and crypto buffers

    How to use them:

    • Export them as metrics and graph them over time, rather than reading them once.
    • A rising heapUsed baseline points to object leaks; rising external with a flat heap points to retained Buffers.
    • Rising rss with flat heap and external figures suggests native memory, such as an addon or memory fragmentation.
    • Keep the heap limit well below the container limit, because rss is always larger than the heap.
  15. Q35.How do you shut down a Node.js server gracefully?

    When the termination signal arrives, stop accepting new connections, let in-flight requests finish within a deadline, close idle keep-alive connections, release resources and exit. A sudden exit drops requests and can leave work half-done.

    flowchart TD
      A["SIGTERM received"] --> B["Mark not ready so traffic stops"]
      B --> C["server.close stops new connections"]
      C --> D["Close idle keep-alive connections"]
      D --> E{"In-flight requests done before deadline?"}
      E -->|yes| F["Close database pools and flush logs"]
      E -->|no| G["Force-close remaining connections"]
      G --> F
      F --> H["Exit with code 0"]

    Details that matter:

    • Register handlers with process.on('SIGTERM', ...) and SIGINT, and make shutdown run only once.
    • server.close() stops accepting connections but waits for open ones; recent versions also close idle keep-alive connections, and closeIdleConnections and closeAllConnections give explicit control.
    • Keep the total shutdown time below the platform's grace period, after which the process is killed.
    • Stop background consumers from taking new jobs, and let unfinished jobs return to their queue.
    • Start Node.js directly as the container's main process, so it actually receives the signal.
  16. Q36.How should a Node.js service handle uncaught exceptions and unhandled promise rejections?

    Log them with full detail, then exit and let a process manager or orchestrator start a fresh process. After an unexpected error, the process may hold half-updated state, leaked resources or broken connections, so continuing is riskier than restarting.

    flowchart TD
      A["Uncaught exception or unhandled rejection"] --> B["Log full details synchronously"]
      B --> C["Stop accepting new requests"]
      C --> D["Let in-flight requests finish within a hard timeout"]
      D --> E["Exit with a non-zero code"]
      E --> F["Supervisor starts a fresh process"]
    • Unhandled rejections: modern Node.js treats them as fatal by default, which surfaces bugs instead of hiding them.
    • uncaughtException handler: use it only to log synchronously and exit with a non-zero code, never to resume normal work.
    • Graceful when possible: stop accepting new requests and give in-flight ones a brief chance to finish before exiting, with a hard timeout.
    • Run several instances: one process restarting must not mean downtime, so keep at least two behind a load balancer.

    Prevent them in the first place:

    1. Await or .catch every promise, and lint for floating promises.
    2. Wrap route handlers so thrown errors reach an error middleware.
    3. Attach error listeners to every stream, socket and emitter you create.
    4. Treat any occurrence in logs as a bug to fix, with an alert on crash restarts.
  17. Q37.How do keep-alive agents work for outgoing HTTP calls in Node.js?

    An HTTP agent manages a pool of sockets to each host. With keep-alive enabled, a finished request's socket goes back to the pool and is reused, which avoids a new TCP and TLS handshake for every call.

    • Why it matters: a fresh TLS connection can add tens of milliseconds and extra CPU per request; reuse removes most of that.
    • Defaults: recent Node.js versions enable keep-alive in the global agent, and the built-in fetch uses its own pooled connections.
    • Pool size: maxSockets limits concurrent connections per host; requests beyond it queue inside the agent.
    • Idle timeouts: idle sockets are closed after a period. If the server closes an idle socket first, the next request on it can fail with a reset.

    Configuring it well:

    1. Create one agent per downstream service and reuse it, rather than a new agent per request.
    2. Set the client's idle timeout shorter than the server's keep-alive timeout, so the client never reuses a socket the server is about to close.
    3. Size maxSockets from expected concurrency, so requests do not queue invisibly.
    4. Retry idempotent requests once on a connection reset, since a reused socket can die between requests.
  18. Q38.How do you measure event loop delay and use it to shed load?

    Event loop delay is how late timers fire compared with when they were scheduled; it rises whenever the main thread is busy. It is the most direct signal that a Node.js process is saturated, more reliable than CPU percentage alone.

    • Measure: perf_hooks.monitorEventLoopDelay() records a histogram of delays; export its 50th and 99th percentiles as metrics.
    • Event loop utilization: performance.eventLoopUtilization() reports the share of time the loop was busy, which complements delay.

    Load shedding:

    1. Choose a threshold, such as a 99th percentile delay above 100 ms.
    2. While it is exceeded, reject new low-priority requests immediately with a 503 status and a retry hint.
    3. Keep health checks and critical endpoints exempt, so the orchestrator does not kill a busy but healthy process.

    Why shedding helps: an overloaded process that accepts everything makes every request slow and many time out anyway. Rejecting some quickly keeps the accepted ones fast, and lets load balancers send traffic elsewhere.

    Alert on rising delay before it reaches the shedding point, and add capacity when shedding happens regularly.

    Example

    A single instance receives a traffic spike, and its 99th percentile event loop delay climbs from 5 ms to 400 ms.

    1. Without shedding, every request waits behind the backlog, median latency rises above 2 seconds, and clients start timing out and retrying, adding more load.
    2. With shedding above 100 ms of delay, about 30% of new requests get an immediate 503 response.
    3. The accepted 70% are served in under 200 ms, and clients retry the rejected ones against other instances.
    4. Autoscaling adds instances, delay falls below the threshold, and shedding stops.
  19. Q39.How do you decide how many Node.js processes to run per server?

    Start with one process per CPU core available to the service, then adjust for memory and for how much CPU each process actually uses. One process cannot use more than about one core for JavaScript, so fewer processes than cores wastes capacity.

    • processes = min(available_cores, floor(usable_memory / memory_per_process))

    What each term means:

    • available_cores: the cores the service may use, which in a container is its CPU limit, not the host's core count.
    • usable_memory: the memory available after the operating system and other software.
    • memory_per_process: the peak resident memory of one process under load, including heap, buffers and native memory.

    Adjustments:

    • Thread-pool work such as hashing and file I/O also uses cores, so leave some headroom when it is heavy.
    • In containers, running one process per container and scaling the number of containers is often simpler, since the orchestrator handles restarts and balancing.
    • Confirm with a load test that throughput actually rises with each added process, and watch event loop delay per process.

    Example

    A server has 8 cores and 16 GB of memory, with about 2 GB reserved for the operating system and agents.

    1. Load testing shows each process peaks at about 900 MB of resident memory.
    2. Memory allows floor(14,000 / 900) = 15 processes.
    3. Cores allow 8, so the smaller limit gives 8 processes.
    4. Because each request also does some password hashing on the thread pool, the team runs 7 processes and keeps one core's worth of headroom.
  20. Q40.What are the pitfalls of in-process caching in a Node.js service?

    An in-process cache, often just a Map, is the fastest cache possible, but it lives inside one process. With several processes or replicas, each holds a different copy, and memory grows with every entry.

    Common pitfalls:

    • Inconsistency: after an update, one process may serve fresh data while another serves the old value until its entry expires.
    • Unbounded growth: a Map keyed by user input, such as a search term, grows until the process runs out of heap.
    • Duplicated memory: ten processes each caching the same 200 MB of data use 2 GB.
    • Stampedes: when a popular entry expires, many concurrent requests miss at once and all hit the database.
    • Cold starts: every new or restarted process starts empty, so deployments cause a burst of misses.

    How to use one safely:

    1. Cache only data that tolerates brief staleness, such as reference data or configuration.
    2. Set a maximum size with least-recently-used eviction, and an expiry time for each entry.
    3. Coalesce concurrent loads for the same key into one promise.
    4. Use a shared cache service when every instance must see the same value or when invalidation must be immediate.