Skip to main content
PProfileMatch

Node.js Interview Questions and Answers

27 questions9 beginner · 12 intermediate · 6 advancedLast reviewed

This set is for developers preparing for backend or full-stack roles that use Node.js, from first-job screens to senior rounds where the event loop, streams and production failure modes get probed. The questions run from runtime basics to scaling, shutdown and API-design trade-offs.

Practise by saying each answer aloud in under a minute before you open it, then compare: did you give the mechanism, a concrete example and one pitfall? Run the code samples yourself on a current Node release, change one line, and predict the output before you rerun. For the underlying ideas, the Node.js course and the REST API course go step by step.

  1. 1What is Node.js, and what kind of workloads is it a good fit for?Beginner

    Node.js is a runtime that runs JavaScript outside the browser. It combines the V8 engine with libuv, which provides the event loop and asynchronous I/O.

    Its sweet spot is I/O-bound work: API servers, proxies, real-time messaging, build tools, anything that spends most of its time waiting on networks or disks. One thread can serve thousands of open connections because it registers a callback and moves on instead of sitting idle.

    The weak spot is long CPU-bound work on the main thread, such as resizing images or transforming a huge JSON document, because it stalls every other request. For that you offload to worker threads, a queue or a separate service.

    What the interviewer is checking: Whether you can describe the runtime accurately and name both its strengths and its limits, not just repeat that it is fast.

    Likely follow-up: Would you build a video-transcoding service in Node.js? How would you structure it if you had to?

  2. 2What is the difference between blocking and non-blocking code in Node.js?Beginner

    Blocking code holds the main thread until it finishes, so nothing else in the process can run. Non-blocking code starts the work, hands back control immediately and gets a callback, promise resolution or event when the result is ready.

    In the sample, the sync read finishes before the next line runs, while the callback version only runs after the current code has completed. In a server, a synchronous call in a request handler freezes every other client for as long as it takes.

    Sync APIs such as readFileSync are fine at start-up, in scripts and in CLIs. They are a bug inside request handling.

    js
    const fs = require("node:fs");
    console.log("1 start");
    fs.readFile(__filename, "utf8", (err, text) => {
      if (err) throw err;
      console.log("3 async read finished");
    });
    const text = fs.readFileSync(__filename, "utf8");
    console.log("2 sync read finished");

    Output

    1 start
    2 sync read finished
    3 async read finished

    What the interviewer is checking: Whether you understand that non-blocking is about not holding the thread, and can say where sync calls are acceptable.

    Likely follow-up: Is await blocking? What happens to the rest of the server while an await is pending?

  3. 3Node is called single-threaded. What does that really mean, and what runs on other threads?Beginner

    Your JavaScript runs on one main thread, one call stack at a time. That is why ordinary variables need no locks, and also why one slow function delays everything.

    The process itself has more threads. libuv keeps a thread pool, four threads by default and adjustable with UV_THREADPOOL_SIZE, which handles file system calls, DNS lookups through the system resolver, and some crypto and zlib work. Network sockets use the operating system's async mechanisms rather than that pool. V8 also uses helper threads for garbage collection.

    A common pitfall is saying only "single-threaded". If five slow pool-backed tasks arrive at once, the fifth waits for a free thread.

    What the interviewer is checking: Depth beyond the slogan: JavaScript execution versus the runtime's own threads.

    Likely follow-up: Four slow crypto.pbkdf2 calls are running. What happens to a fifth one, and to a file read issued right after?

  4. 4What are the main differences between CommonJS and ES modules in Node.js?Beginner

    CommonJS uses require() and module.exports. It loads synchronously, and require is an ordinary function call, so you can use it conditionally. ES modules use import/export, are statically analysable, load asynchronously and allow top-level await.

    A file is treated as ESM if it ends in .mjs or its nearest package.json has "type": "module". Inside ESM, require, module, __dirname and __filename do not exist; you derive paths from import.meta.url. Relative imports must include the file extension.

    Interop is one-sided in older releases: ESM can import CommonJS, but require() of an ES module was an error. Recent Node versions have been relaxing that, so check what your target version supports.

    What the interviewer is checking: Practical awareness of how the module system is chosen and what disappears in ESM.

    Likely follow-up: Your package is CommonJS but a dependency is ESM-only. What are your options?

  5. 5What is a Buffer in Node.js and why do we need it?Beginner

    A Buffer is a fixed-size chunk of raw bytes, a Uint8Array subclass. JavaScript strings are text, but files, sockets and cryptography deal in bytes, so Node needs a way to hold and manipulate binary data.

    The key point is that bytes and characters are different things. In UTF-8, "café" is four characters but five bytes. If a network chunk ends in the middle of a multi-byte character and you convert each chunk to a string separately, you get replacement characters. Use setEncoding on the stream or a StringDecoder, or concatenate the buffers first.

    Prefer Buffer.from and Buffer.alloc; the old new Buffer() constructor is deprecated.

    js
    const b = Buffer.from("café", "utf8");
    console.log(b.length, "café".length);
    console.log(b.toString("hex"));
    const { StringDecoder } = require("node:string_decoder");
    const first = b.subarray(0, 4);   // ends in the middle of "é"
    const second = b.subarray(4);
    console.log(JSON.stringify(first.toString() + second.toString()));
    const dec = new StringDecoder("utf8");
    console.log(JSON.stringify(dec.write(first) + dec.write(second)));

    Output

    5 4
    636166c3a9
    "caf��"
    "café"

    What the interviewer is checking: Whether you separate bytes from characters and know the chunk-boundary pitfall.

    Likely follow-up: How would you safely read a request body that arrives as many chunks and parse it as JSON?

  6. 6How do you handle errors in async/await code? What is wrong with return promise inside a try/catch?Beginner

    With async/await you wrap awaited calls in try/catch, or attach .catch when you are using promises directly. The catch only sees rejections of promises that are awaited inside the try block.

    That is the trap with return loadParcel(1). The function returns the pending promise without waiting, so the try block has already exited before it rejects, and the catch never runs. return await loadParcel(1) keeps the rejection inside the block. If you have a finally that must run after the work, the same applies.

    Also remember an async function never throws synchronously; it always returns a promise, so callers must await or catch it.

    js
    async function loadParcel(id) {
      throw new Error("parcel " + id + " not found");
    }
    
    async function withoutAwait() {
      try {
        return loadParcel(1);          // rejection leaves the try block unobserved
      } catch (e) {
        return "handled inside";
      }
    }
    async function withAwait() {
      try {
        return await loadParcel(1);
      } catch (e) {
        return "handled inside";
      }
    }
    
    (async () => {
      console.log(await withoutAwait().catch((e) => "escaped: " + e.message));
      console.log(await withAwait().catch((e) => "escaped: " + e.message));
    })();

    Output

    escaped: parcel 1 not found
    handled inside

    What the interviewer is checking: Whether you know exactly when a rejection is observed, which is where many real bugs hide.

    Likely follow-up: When would you deliberately skip the await in a return, and what do you lose?

  7. 7What is middleware in an Express-style framework, and what does next() do?Beginner

    Middleware is a function that receives the request, the response and a next callback. It can read or modify the request, end the response itself, or call next() to pass control to the following function in the chain.

    Think of it as a pipeline each request walks through: logging, parsing the body, authentication, then the route handler. If a middleware neither responds nor calls next(), the request hangs until the client times out.

    Calling next(err) with an argument skips ahead to error-handling middleware, which is recognised by its four parameters (err, req, res, next). Common use cases are authentication, validation, compression, rate limiting and request IDs.

    What the interviewer is checking: That you understand the control flow of a request through the stack, including the hanging-request failure mode.

    Likely follow-up: How would you write a middleware that measures how long each request takes and logs it?

  8. 8How do you manage configuration and secrets in a Node.js application?Beginner

    Follow the twelve-factor idea: configuration that changes between environments comes from environment variables, not from code or committed files. Secrets such as database passwords and signing keys are injected by the platform, a secrets manager or CI, never committed to git.

    Load and validate everything once at start-up, in one module, and fail fast with a clear message if something is missing or malformed. That turns a mysterious runtime failure at 2am into a failed deploy. Environment values are always strings, so convert and range-check numbers and booleans.

    For local development, recent Node versions can read a .env file with --env-file, or you can use a library such as dotenv. Keep the .env file out of version control.

    What the interviewer is checking: Operational maturity: fail-fast validation and keeping secrets out of the repository.

    Likely follow-up: Why is it risky to log the whole config object at start-up?

  9. 9In a REST API, how do GET, POST, PUT, PATCH and DELETE differ? Which are idempotent?Beginner

    GET reads a resource and must have no side effects. POST creates a resource or triggers an action, so repeating it can create duplicates. PUT replaces a resource at a known URL with the body you send. PATCH applies a partial change. DELETE removes a resource.

    Idempotent means that making the same request several times leaves the server in the same state as making it once. GET, PUT and DELETE are idempotent by definition; so is PATCH in practice if you write it as "set this field to X", but not if it means "add 1". POST is not. Safe methods like GET also should not change anything.

    Idempotency matters because clients and proxies retry on timeouts, so it decides which calls are safe to repeat automatically.

    What the interviewer is checking: Whether you know the semantics and can connect idempotency to retry safety.

    Likely follow-up: A client times out on a POST that creates an order. How do you make a retry safe?

  10. 10Walk me through the phases of the Node.js event loop. What do you actually need to know in practice?Intermediate

    Each loop iteration visits phases in order: timers (expired setTimeout/setInterval callbacks), pending callbacks, internal idle/prepare, poll (collect new I/O events and run their callbacks, waiting here if nothing else is due), check (setImmediate callbacks) and close callbacks.

    Between callbacks, Node drains the process.nextTick queue and then the promise microtask queue. So a promise continuation always runs before the loop moves on.

    Practical consequences: a timer delay is a minimum, not a promise, because a busy loop delays it. Inside an I/O callback, setImmediate always runs before a zero-delay timer, because check follows poll. And a callback that runs for 300 ms delays every timer and every socket behind it.

    What the interviewer is checking: Whether you can map the phases onto observable behaviour instead of reciting a diagram.

    Likely follow-up: A setTimeout of 100 ms fires after 400 ms in production. What could cause that?

  11. 11What is the order of process.nextTick, a resolved promise and setImmediate? How do they differ?Intermediate

    process.nextTick callbacks run first, right after the current operation, before the event loop continues. Promise callbacks (microtasks) run next. setImmediate runs later, in the check phase of the loop. In a CommonJS script that prints sync, nextTick, promise, setImmediate.

    One detail: in an ES module, the main module itself is evaluated inside a promise job, so the promise callback can run before nextTick. Do not build logic on that difference.

    The warning is starvation. A nextTick or promise that keeps rescheduling itself prevents the loop from ever reaching I/O. Use setImmediate to yield to I/O, and keep nextTick for rare cases like emitting an event after the caller has attached listeners.

    js
    setImmediate(() => console.log("setImmediate"));
    Promise.resolve().then(() => console.log("promise"));
    process.nextTick(() => console.log("nextTick"));
    console.log("sync");

    Output

    sync
    nextTick
    promise
    setImmediate

    What the interviewer is checking: Whether you know the three queues, their priority and the starvation risk.

    Likely follow-up: Why is the order of setTimeout(fn, 0) and setImmediate unreliable in the main module but fixed inside an I/O callback?

  12. 12Why use streams instead of reading a whole file into memory? How do you chain them safely?Intermediate

    Streams process data in chunks, so memory stays flat no matter how large the input is, and the first bytes can leave before the last ones are read. Reading a multi-gigabyte export with readFile per request would hold the whole thing in memory for each concurrent user.

    There are four kinds: Readable, Writable, Duplex and Transform. To connect them, prefer stream.pipeline (or its promise version) over chained .pipe() calls. With plain .pipe() an error in one stream does not close the others, so file handles and sockets can leak. pipeline forwards the error and destroys every stream in the chain.

    Readable streams are also async iterables, so for await (const chunk of stream) is a clean way to consume them.

    What the interviewer is checking: Whether you know why streams exist and the error-handling advantage of pipeline.

    Likely follow-up: How would you stream a large database result set to an HTTP client as CSV?

  13. 13What is backpressure in streams, and how do you respect it when writing?Intermediate

    Backpressure is the signal a slow consumer sends upstream to say "stop sending, I am full". In Node, writable.write(chunk) returns false once its internal buffer has reached highWaterMark. You should stop writing and resume when the drain event fires.

    The return value is advice, not enforcement. Node will keep accepting data after false, so a fast producer feeding a slow destination silently buffers everything in memory until the process dies.

    pipe() and pipeline() implement this automatically, which is another reason to use them. You only handle it by hand when writing your own loop, as in the sample, where the tiny buffer makes the pauses visible.

    js
    const { Writable } = require("node:stream");
    const sink = new Writable({
      highWaterMark: 4,
      write(chunk, _enc, done) { setTimeout(done, 5); },
    });
    let sent = 0;
    function pump() {
      while (sent < 10) {
        const ok = sink.write("x");
        sent++;
        if (!ok) {
          console.log("paused after", sent, "writes");
          sink.once("drain", pump);
          return;
        }
      }
      console.log("all", sent, "written");
      sink.end();
    }
    pump();

    Output

    paused after 4 writes
    paused after 8 writes
    all 10 written

    What the interviewer is checking: Whether you understand the mechanism and the consequence of ignoring it, not only the term.

    Likely follow-up: What does highWaterMark mean for a stream in object mode?

  14. 14Why does middleware order matter in Express-style apps? Give an example of an ordering bug.Intermediate

    Middleware runs in the order it is registered, and each one decides whether to call next() or finish the response. So the order is part of the design. A sensible stack is request ID and logging, security headers and CORS, body parsing, rate limiting and authentication, then routes, then a 404 handler, then the error handler last.

    Typical bugs: a body parser registered after the route that needs req.body; an auth check registered after the routes it should protect; or, as in the sample, a rate limiter placed after the handler that already responded, so it never runs.

    In older Express versions a rejected promise in an async handler does not reach the error middleware unless you call next(err) or wrap it. Recent major versions forward it, but verify for yours.

    js
    function createApp() {
      const stack = [];
      return {
        use(fn) { stack.push(fn); },
        handle(req) {
          const res = { trail: [] };
          let i = 0;
          const next = () => {
            const fn = stack[i++];
            if (fn) fn(req, res, next);
          };
          next();
          return res.trail;
        },
      };
    }
    
    const app = createApp();
    app.use((req, res, next) => { res.trail.push("logger"); next(); });
    app.use((req, res, next) => {
      if (!req.token) { res.trail.push("auth: 401"); return; }   // no next(): chain stops
      res.trail.push("auth: ok");
      next();
    });
    app.use((req, res) => { res.trail.push("route handler"); });  // sends the response
    app.use((req, res, next) => { res.trail.push("rate limiter"); next(); }); // too late
    
    console.log(app.handle({ token: "abc" }));
    console.log(app.handle({}));

    Output

    [ 'logger', 'auth: ok', 'route handler' ]
    [ 'logger', 'auth: 401' ]

    What the interviewer is checking: Whether you can reason about a request's path through a middleware stack and spot ordering mistakes.

    Likely follow-up: Where would you put a middleware that attaches a correlation ID, and why there?

  15. 15What happens to an unhandled promise rejection in modern Node.js, and how should you deal with it?Intermediate

    Since Node 15 the default is to treat an unhandled rejection like an uncaught exception: the process prints the error and exits with a non-zero code. Older versions only warned, which hid bugs.

    The real fix is to make sure every promise is awaited or has a .catch. Fire-and-forget calls such as sending an analytics event need their own catch that logs the failure.

    A process.on("unhandledRejection") handler is a safety net for logging and metrics, not a way to keep running. After an unexpected error the process state is uncertain, so log it, shut down gracefully and let the supervisor restart you. In the sample the handler stops the crash, and exitCode = 1 records that something went wrong.

    js
    process.on("unhandledRejection", (reason) => {
      console.log("unhandled:", reason.message);
      process.exitCode = 1;
    });
    
    async function refreshRates() {
      throw new Error("rates API timed out");
    }
    
    refreshRates(); // no await, no catch
    setTimeout(() => console.log("process is still alive"), 20);

    Output

    unhandled: rates API timed out
    process is still alive

    What the interviewer is checking: Whether you know the current default behaviour and treat the handler as a last resort.

    Likely follow-up: What is the difference between handling unhandledRejection and uncaughtException?

  16. 16What baseline security measures would you add to a Node.js HTTP API?Intermediate

    Start with transport and headers: serve over HTTPS and set hardening headers, which is what helmet-style middleware does, such as X-Content-Type-Options: nosniff, Strict-Transport-Security and a Content-Security-Policy for any HTML you serve. Hide the framework banner, for example with app.disable("x-powered-by").

    Then: a strict CORS allowlist instead of * for credentialed requests, body-size limits, schema validation on every input, parameterised database queries, rate limiting on login and expensive endpoints, and error responses that never leak stack traces.

    Operationally, keep dependencies patched with npm audit or similar tools, run as a non-root user and keep secrets out of logs. The OWASP Top 10 course maps these to concrete attacks.

    What the interviewer is checking: Breadth across headers, input handling and operations, with the reason behind each measure.

    Likely follow-up: What does CORS actually protect, and what does it not protect against?

  17. 17How do you validate request bodies in Node.js, and why is a TypeScript type not enough?Intermediate

    TypeScript types are erased at compile time. Whatever JSON a client sends arrives as untyped data, so the type annotation on req.body is only a hope. Validate at the edge of the system, before any business logic runs.

    Check shape, types, lengths and ranges, and allowlist the fields you accept. Rejecting unknown fields prevents mass-assignment bugs, such as a caller sneaking in isAdmin: true. Return 400 or 422 with per-field messages, and do not silently "fix" bad input.

    Schema libraries such as zod, joi or ajv do this declaratively, or you can write an allowlist check by hand. Validation is not a substitute for parameterised queries or output encoding, which defend different layers.

    What the interviewer is checking: Whether you treat runtime input as untrusted and know about allowlisting and mass assignment.

    Likely follow-up: Should validation live in middleware, in the handler or in the service layer?

  18. 18How would you implement rate limiting in a Node.js API, and what are the limits of a simple approach?Intermediate

    The simplest version is a fixed-window counter per key: allow N requests per window, reject the rest with 429 and a Retry-After header. The key is usually the API key or user ID, with the client IP as a fallback. Behind a proxy you must configure trusted proxies so you read the real IP.

    Weaknesses: a client can burst at the boundary between windows and get nearly double the limit; the in-memory map grows with distinct keys and needs cleanup; and each process counts separately, so four instances allow four times the limit. Sliding window or token bucket smooth the burst, and a shared store such as Redis fixes the multi-instance problem.

    Rate limiting is also a defence for login endpoints. See the rate limiting course.

    js
    function createLimiter({ limit, windowMs, now = Date.now }) {
      const hits = new Map(); // key -> { count, resetAt }
      return function allow(key) {
        const t = now();
        const entry = hits.get(key);
        if (!entry || t >= entry.resetAt) {
          hits.set(key, { count: 1, resetAt: t + windowMs });
          return true;
        }
        if (entry.count >= limit) return false;
        entry.count++;
        return true;
      };
    }
    
    let clock = 0;
    const allow = createLimiter({ limit: 3, windowMs: 1000, now: () => clock });
    const results = [];
    for (let i = 0; i < 5; i++) results.push(allow("203.0.113.9"));
    clock = 1000;                      // next window
    results.push(allow("203.0.113.9"));
    console.log(results);

    Output

    [ true, true, true, false, false, true ]

    What the interviewer is checking: Whether you can build a basic limiter and name its real weaknesses, especially across instances.

    Likely follow-up: Compare token bucket with sliding window. When would you pick each?

  19. 19How do you choose HTTP status codes for errors? Compare 400 and 422, 401 and 403, 404 and 409.Intermediate

    Use the code that tells the client what to do next. 400 means the request is malformed; 422 is commonly used when the syntax is fine but the content fails validation rules. Teams differ, so pick one convention and document it.

    401 means "I do not know who you are": missing or invalid credentials. 403 means "I know who you are and you may not do this". 404 means the resource does not exist, and is sometimes used deliberately instead of 403 so you do not reveal that a record exists. 409 signals a conflict with current state, such as a duplicate booking or a stale version.

    Return a consistent JSON error body with a machine-readable code, a human message and field details, and never use 200 for failures. Rate limits are 429. The REST API course covers error design.

    What the interviewer is checking: Judgement about API contracts and consistency, not memorising a table.

    Likely follow-up: How would you design error responses so mobile clients can show a field-level message?

  20. 20Offset or cursor pagination for a list endpoint? How do you decide?Intermediate

    Offset pagination (?page=3&limit=20, or OFFSET in SQL) is simple and lets users jump to a page. But the database still has to walk past all skipped rows, so deep pages get slow, and if rows are inserted or deleted while the user pages, items are skipped or repeated.

    Cursor (keyset) pagination returns an opaque token that encodes the last item's sort key, for example created_at plus id, and the next query asks for rows after it. It stays fast on large tables with an index and is stable under inserts, but you cannot jump to page 50 and the sort must be deterministic, so always add a unique tiebreaker.

    Use offset for small admin tables, cursors for feeds and large datasets. Return the next cursor in the response body or a Link header.

    What the interviewer is checking: Whether you can connect an API design choice to database behaviour and consistency.

    Likely follow-up: How would you support sorting by a non-unique column such as price with cursor pagination?

  21. 21When would you use the cluster module and when worker_threads? What is the difference?Intermediate

    cluster forks several full Node processes that can share one server port, so you use all CPU cores for handling HTTP requests. Each process has its own memory, so an in-memory cache, session map or counter is not shared between them.

    worker_threads runs extra threads inside one process, each with its own V8 isolate and event loop. It suits CPU-heavy jobs like image processing, parsing or hashing, since the main thread stays responsive. Threads communicate by message passing, transferable buffers or SharedArrayBuffer.

    Neither helps I/O-bound code, which the event loop already handles. In containerised deployments many teams skip cluster and run one process per container, letting the orchestrator scale replicas. Use child_process when you need to run an external program.

    What the interviewer is checking: Whether you can match the tool to the bottleneck and know the shared-state consequence.

    Likely follow-up: Why will an in-memory session store break as soon as you enable cluster mode?

  22. 22Your API's latency spikes for every endpoint at once and one CPU core is pinned. How do you diagnose and fix it?Advanced

    That pattern points to the event loop being blocked by CPU work on the main thread. Confirm it by measuring event-loop delay (perf_hooks.monitorEventLoopDelay) and by taking a CPU profile with --cpu-prof or the inspector, then read the flame graph for the hot function.

    Usual culprits: synchronous crypto or file calls, parsing or serialising huge JSON, large in-memory sorts, and regular expressions with catastrophic backtracking. Fixes, in order: remove the work or make the algorithm cheaper; split it into chunks and yield with setImmediate; move it to a worker thread pool; or push it to a queue and a separate service.

    The sample hands a Fibonacci calculation to a worker, so the main thread keeps serving. In real code reuse a pool of workers instead of spawning one per request.

    js
    const { Worker, isMainThread, parentPort, workerData } = require("node:worker_threads");
    
    function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
    
    if (isMainThread) {
      const worker = new Worker(__filename, { workerData: 32 });
      worker.on("message", (v) => console.log("main: result", v));
      console.log("main: still free to serve requests");
    } else {
      parentPort.postMessage(fib(workerData));
    }

    Output

    main: still free to serve requests
    main: result 2178309

    What the interviewer is checking: A systematic approach: measure, find the hot path, then pick the least costly fix.

    Likely follow-up: How would you detect and prevent a ReDoS-prone regular expression in code review?

  23. 23A Node service's memory climbs steadily until it is killed. How do you find the leak?Advanced

    First confirm it is a leak, not normal growth: watch process.memoryUsage().heapUsed over time under steady load. Memory that climbs and never falls after garbage collection is a leak; a plateau is just a large working set.

    Then compare heap snapshots. Take one, apply load, take another (via --inspect and Chrome DevTools, or v8.writeHeapSnapshot()), and look at which object types grew and what retains them.

    Common causes are unbounded caches or maps, listeners added per request and never removed, closures that capture large objects, arrays that only grow, and timers that are never cleared. The listener case is easy to spot because Node prints a MaxListenersExceededWarning once an emitter passes ten listeners for one event. Fix with bounded caches (LRU or TTL), removing listeners, or WeakMap for per-object metadata.

    What the interviewer is checking: Whether you have a method (confirm, snapshot, diff, fix) and know the usual suspects.

    Likely follow-up: What is the difference between a memory leak and the process simply needing a larger heap limit?

  24. 24How would you implement graceful shutdown for a Node HTTP server running behind a load balancer?Advanced

    On SIGTERM, stop taking new work but finish what is in flight. Fail the readiness probe first, so the load balancer stops routing to you, then call server.close(), wait for active requests to finish, close database pools and queue consumers, and exit.

    server.close() only calls its callback once every connection has ended, and idle keep-alive sockets can hold it open, so also call server.closeIdleConnections(), available in recent versions. Add a hard deadline timer so a stuck request cannot block the deploy forever.

    The pitfall is calling process.exit() immediately, which kills in-flight requests. Make the deadline timer unrefed so it does not keep the process alive on its own.

    What the interviewer is checking: Production awareness: signals, draining, readiness and a hard deadline.

    Likely follow-up: How does WebSocket traffic change your shutdown plan?

  25. 25How do you make a POST endpoint that charges a payment safe against client retries?Advanced

    Use an idempotency key. The client generates a unique key per logical operation and sends it in a header such as Idempotency-Key. The server records the key with the result of the first execution, and any retry with the same key returns that stored result instead of charging again.

    Two details matter. Concurrent duplicates must collapse too, so reserve the key atomically (a unique constraint or a compare-and-set) before doing the work. The sample stores the promise for that reason. And reject a reused key with a different request body, typically with 409 or 422.

    Production storage must be shared and durable, such as a database table or Redis with a TTL, not process memory, because retries can hit another instance.

    js
    const results = new Map(); // idempotency key -> promise of the first result
    let charges = 0;
    
    async function charge(amount) {
      charges++;
      await new Promise((r) => setTimeout(r, 10));
      return { chargeId: "ch_" + charges, amount };
    }
    
    function createPayment(key, amount) {
      if (!results.has(key)) results.set(key, charge(amount));
      return results.get(key);   // a retry (even a concurrent one) gets the same promise
    }
    
    (async () => {
      const [a, b] = await Promise.all([createPayment("k-1", 500), createPayment("k-1", 500)]);
      console.log(a.chargeId, b.chargeId, "charges made:", charges);
    })();

    Output

    ch_1 ch_1 charges made: 1

    What the interviewer is checking: Whether you can turn an at-least-once network into effectively-once behaviour and handle the race.

    Likely follow-up: What should happen if the first attempt failed with a server error? Do you store that outcome?

  26. 26How do you add timeouts and cancellation to outbound calls in Node.js?Advanced

    Every outbound call needs a deadline. Without one, a slow dependency makes requests pile up, holding sockets and memory until your service falls over with it. The modern tool is AbortController: pass its signal to the operation and call abort() when time is up or the client disconnects.

    AbortSignal.timeout(ms) creates a signal that aborts itself, and fetch, timers/promises, streams and fs/promises all accept a signal. An aborted operation rejects with an AbortError, which you usually treat differently from a real failure.

    Remember that aborting only stops your side. The remote server may already have performed the work, which is why retries on non-idempotent calls need idempotency keys.

    What the interviewer is checking: Resilience thinking: deadlines everywhere, and awareness that cancellation is not rollback.

    Likely follow-up: How would you propagate one request-level deadline to several downstream calls?

  27. 27Your Node API works on one server but misbehaves on four behind a load balancer. What usually breaks, and how do you fix it?Advanced

    The cause is almost always state kept in process memory. Sessions, caches, rate-limit counters, WebSocket rooms, uploaded files on local disk and scheduled jobs all behave differently when requests land on different instances. Cron-style jobs may run four times.

    Make the services stateless and move shared state out. Use stateless tokens such as JWTs (see the JWT authentication course) or a shared session store; keep counters and caches in Redis; send uploads to object storage; run schedulers on one elected instance or in a dedicated worker.

    WebSockets need either sticky routing or a pub/sub backplane so a message published on one instance reaches clients connected to another. Test with at least two instances locally, because single-instance testing hides all of this.

    What the interviewer is checking: Whether you think in terms of horizontal scaling and can list concrete stateful features.

    Likely follow-up: How do you roll out a change to a shared cache's data format with zero downtime?

Fill the gaps with a free course

If one of these Node.js answers felt shaky, these 3 courses cover the same ground step by step: Node.js, REST API Development, JWT Authentication.

More interview questions

Practising Node.js for a real opening? Upload your resume to see which open jobs list Node.js skills you already have, or browse open roles.