← All practice sets
FoundationsPractice · 12 questions

Node.js interview questions

Node.js backend questions from the event loop down to production: thread pool, streams, CPU-bound work, errors, Express and Fastify, shutdown, leaks and security basics.

Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.

  1. Q1Mid

    Walk through the phases of the Node.js event loop. Where do timers, I/O callbacks and setImmediate run?

    Reveal model answer

    Each loop iteration goes through timers (expired setTimeout and setInterval callbacks), pending callbacks (some deferred system I/O callbacks), idle/prepare (internal), poll (wait for and run I/O callbacks), check (setImmediate callbacks) and close callbacks. Between callbacks, Node drains the process.nextTick queue and then the promise microtask queue. The poll phase is where the process blocks waiting for I/O when there is nothing else to do, and a slow synchronous callback in any phase delays everything else.

    WHAT A STRONG ANSWER COVERS

    • Order: timers, pending callbacks, idle/prepare, poll, check, close callbacks.
    • nextTick and promise microtasks run between callbacks, not as a phase.
    • Timer delays are minimums; a busy loop runs them late.
    • Network I/O is handled by the OS (epoll, kqueue, IOCP) via libuv, not by threads per request.

    RED FLAGS

    • Saying Node is single-threaded in every sense, with no thread pool.
    • Saying each request gets its own thread.

    LIKELY FOLLOW-UPS

    • Why can setImmediate be preferred over setTimeout(fn, 0) for yielding?
    • How would you measure event loop lag in production?

    SOURCES

  2. Q2Mid

    In a CommonJS file, what does this print? `setTimeout(() => console.log("timeout"), 0); setImmediate(() => console.log("immediate")); Promise.resolve().then(() => console.log("promise")); process.nextTick(() => console.log("tick")); console.log("sync");`

    Reveal model answer

    It prints sync, tick, promise, and then timeout and immediate in an order that is not guaranteed from the main module, because it depends on whether the 1 ms timer has expired when the loop starts. In CommonJS the nextTick queue drains before the promise microtask queue. Inside an I/O callback, setImmediate always runs before a 0 ms timeout because the check phase comes right after poll. In an ES module the main body itself runs as part of a microtask, so the promise callback logs before the nextTick one.

    WHAT A STRONG ANSWER COVERS

    • Synchronous code first, then nextTick, then promise microtasks (in CommonJS).
    • setTimeout 0 vs setImmediate is nondeterministic in the main module.
    • Inside an I/O callback, setImmediate wins.
    • ESM flips nextTick and promise ordering for code in the module body.
    • Recursive nextTick can starve I/O; queueMicrotask is the portable choice for deferral.

    RED FLAGS

    • Claiming setImmediate means 'run immediately', before nextTick.
    • Stating a fixed order for timeout vs immediate at top level.

    LIKELY FOLLOW-UPS

    • Why are the names nextTick and setImmediate considered misleading?
    • When is process.nextTick still the right tool?

    SOURCES

  3. Q3Senior

    What runs on the libuv thread pool, and how can it become a hidden bottleneck?

    Reveal model answer

    The pool (default size 4, set with UV_THREADPOOL_SIZE at startup) runs work with no good async OS interface: fs operations, dns.lookup, and CPU-heavy crypto and zlib calls such as pbkdf2, scrypt, randomBytes and compression. Network sockets do not use it. Because it is small and shared, a burst of password hashing or large file reads can queue behind four threads and delay unrelated work, for example every outbound HTTP request that resolves a hostname through dns.lookup. Symptoms are high latency with low CPU and a healthy event loop.

    WHAT A STRONG ANSWER COVERS

    • fs, dns.lookup, some crypto and zlib use the pool; TCP/HTTP sockets do not.
    • Default 4 threads, shared across the whole process.
    • Saturation shows as latency without event-loop lag.
    • Raise UV_THREADPOOL_SIZE before the pool is first used, or move heavy work out.
    • dns.resolve* uses c-ares on the network rather than the pool.

    RED FLAGS

    • Saying all async I/O in Node uses the thread pool.
    • Saying Node has no threads.

    LIKELY FOLLOW-UPS

    • Why might bcrypt in a login route slow down unrelated file uploads?
    • How would you size the pool, and what does more threads cost?

    SOURCES

  4. Q4Senior

    What is backpressure in streams? What is wrong with `readable.on("data", (chunk) => writable.write(chunk));`?

    Reveal model answer

    Backpressure is the mechanism that slows a fast producer down to the speed of a slow consumer. `write()` returns false once the writable's buffer passes its highWaterMark, meaning 'stop until drain'. The snippet ignores that return value, so when the destination is slower (a slow client, disk or gzip) chunks pile up in memory until the process runs out of heap. Use `pipeline` from `node:stream/promises`, which respects backpressure, propagates errors and destroys every stream on failure.

    WHAT A STRONG ANSWER COVERS

    • write() returning false means pause; resume on 'drain'.
    • highWaterMark is a buffering threshold, not a hard limit.
    • pipeline handles backpressure, errors and cleanup; bare pipe() does not forward errors.
    • Async iteration (`for await` over a readable) is another backpressure-friendly pattern.
    • Streaming keeps memory flat for large files and responses.

    RED FLAGS

    • Reading entire large files into memory to process them.
    • Using .pipe() without error handling, leaking file descriptors on failure.

    LIKELY FOLLOW-UPS

    • How would you stream a large CSV export from Postgres to an HTTP response?
    • What happens to the pipeline if the client disconnects midway?

    SOURCES

  5. Q5Mid

    An endpoint parses a 50 MB JSON payload and computes a report, and every other request slows down while it runs. Why, and what are your options?

    Reveal model answer

    JavaScript callbacks run on one main thread, so a long synchronous computation like JSON.parse on a huge body, a heavy loop or a catastrophic regex blocks the event loop and every other request waits. Options are: bound the input size, partition the work into chunks that yield with setImmediate, move it to worker_threads (ideally a pool such as Piscina), push it to a background job queue, or use a streaming parser. The right choice depends on whether the caller needs the result synchronously.

    WHAT A STRONG ANSWER COVERS

    • Async does not mean parallel; CPU work blocks the loop.
    • Limit request body size and validate before heavy parsing.
    • worker_threads for CPU-bound JavaScript; job queues for work that can be asynchronous to the user.
    • Watch for ReDoS and JSON.parse/stringify on large payloads.
    • Measure with event loop delay (perf_hooks.monitorEventLoopDelay) and CPU profiles.

    RED FLAGS

    • Wrapping synchronous CPU work in a Promise or async function and calling it non-blocking.
    • Spawning a new worker per request with no pool.

    LIKELY FOLLOW-UPS

    • How do you move a large buffer to a worker without copying it?
    • When is a separate service better than a worker thread?

    SOURCES

  6. Q6Senior

    What is the difference between the `cluster` module and `worker_threads`? When would you use each?

    Reveal model answer

    cluster forks separate processes that share a listening port, with the primary distributing connections (round-robin by default except on Windows); each has its own memory and event loop, so it scales an I/O-bound server across cores and isolates crashes, but shares no state. worker_threads run additional V8 isolates inside one process, communicating by message passing with transferable or SharedArrayBuffer memory; they suit CPU-bound JavaScript, not I/O. In containers, running one process per container and scaling replicas often replaces cluster entirely.

    WHAT A STRONG ANSWER COVERS

    • cluster: processes, separate heaps, shared port, crash isolation.
    • worker_threads: threads in one process, separate isolates, message passing, optional shared memory.
    • Workers do not speed up I/O-bound work.
    • Neither shares in-memory sessions or caches; use external state.
    • Orchestrators (Kubernetes, PM2) often handle the multi-process part.

    RED FLAGS

    • Thinking worker threads share ordinary JS objects.
    • Using cluster and keeping sessions in process memory.

    LIKELY FOLLOW-UPS

    • How would you share a rate-limit counter across cluster workers?
    • What is the cost of starting a worker thread?

    SOURCES

  7. Q7Mid

    What happens in modern Node.js when a promise rejection is never handled, and how should a service deal with uncaught errors?

    Reveal model answer

    Since Node 15 the default `--unhandled-rejections` mode is `throw`: an unhandled rejection is raised as an uncaught exception and, without a handler, the process exits with a non-zero code. `uncaughtException` handlers are a last resort for logging and synchronous cleanup, not for carrying on, because the process is in an unknown state. The right pattern is to handle errors where they occur, log and exit on anything truly uncaught, and let the supervisor (systemd, Kubernetes, PM2) restart the process.

    WHAT A STRONG ANSWER COVERS

    • Default is to crash on unhandled rejections.
    • Do not resume after uncaughtException; log, clean up, exit.
    • Rely on a process supervisor for restarts.
    • Common sources: missing await, fire-and-forget promises, async event handlers.

    RED FLAGS

    • Adding a global handler that logs and keeps serving.
    • Running with --unhandled-rejections=warn to stop crashes.

    LIKELY FOLLOW-UPS

    • How do you safely fire and forget a background task?
    • How do EventEmitter 'error' events differ from rejections?

    SOURCES

  8. Q8Mid

    Errors in this Express app return the default HTML error page instead of the JSON handler. Why? `app.use((err, req, res, next) => res.status(500).json({ error: "internal" })); app.use(express.json()); app.use("/users", usersRouter);`

    Reveal model answer

    Express runs middleware in registration order, and error-handling middleware only receives errors raised by middleware registered before it. Here the handler is registered first, so errors from the routes skip it and fall through to Express's default handler. Register the four-argument error handler last, after all routes. Express identifies error middleware by its arity, so it must declare all four parameters even if `next` is unused, and it should defer to `next(err)` when `res.headersSent` is true.

    WHAT A STRONG ANSWER COVERS

    • Middleware runs in order of `app.use`/route registration.
    • Error middleware has exactly four parameters and goes last.
    • Call `next(err)` to jump to error handlers; `next()` continues normally.
    • Express 5 forwards rejected promises from async handlers to next(err); Express 4 needed wrappers.
    • Check res.headersSent before writing an error response.

    RED FLAGS

    • Writing the error handler with three parameters.
    • Forgetting to call next() in a middleware, leaving requests hanging.

    LIKELY FOLLOW-UPS

    • How would you add a 404 handler, and where does it go?
    • How do you attach a request ID to every log line?

    SOURCES

  9. Q9Mid

    Express or Fastify for a new API? What are the real differences?

    Reveal model answer

    Express is minimal and unopinionated with the largest middleware ecosystem; you assemble validation, logging and structure yourself. Fastify is built around schemas and plugins: JSON Schema validates requests via Ajv, response schemas compile fast serializers that also drop undeclared fields, an encapsulated plugin system scopes decorators and hooks, and it ships with a structured logger. Fastify is faster in benchmarks, but for most APIs the database dominates latency, so team familiarity and the built-in structure matter more than raw throughput.

    WHAT A STRONG ANSWER COVERS

    • Express: minimal core, huge ecosystem, middleware chain.
    • Fastify: schema-first validation and serialization, hooks lifecycle, encapsulated plugins.
    • Response schemas in Fastify prevent accidental data leakage.
    • Framework overhead is rarely the bottleneck compared with I/O.

    RED FLAGS

    • Choosing purely on a hello-world benchmark.
    • Claiming Express cannot handle production load.

    LIKELY FOLLOW-UPS

    • How would you migrate an Express app incrementally?
    • What does encapsulation in Fastify plugins buy you?

    SOURCES

  10. Q10Senior

    How do you shut down a Node.js HTTP service gracefully during a deploy?

    Reveal model answer

    Listen for SIGTERM (installing a listener removes Node's default exit), mark the instance as not ready so the load balancer stops routing to it, and call `server.close()` to stop accepting connections; since Node 19 it also closes idle keep-alive connections. Let in-flight requests finish, stop consuming from queues and finish current jobs, then close database pools and other clients, and exit. Enforce a deadline shorter than the orchestrator's grace period, after which you call `server.closeAllConnections()` and exit non-zero.

    WHAT A STRONG ANSWER COVERS

    • Handle SIGTERM (and SIGINT locally); a listener replaces default behaviour.
    • Fail readiness first so traffic drains.
    • server.close() stops new connections; in-flight requests complete.
    • Drain queue consumers and close pools in dependency order.
    • A hard timeout prevents hanging forever on stuck sockets.

    RED FLAGS

    • Calling process.exit() immediately on SIGTERM.
    • Running node behind npm or a shell that does not forward signals.
    • No timeout, so a long-lived connection blocks shutdown until SIGKILL.

    LIKELY FOLLOW-UPS

    • How do you handle WebSocket connections during shutdown?
    • Why can an npm start wrapper swallow SIGTERM in a container?

    SOURCES

  11. Q11Senior

    A Node.js service's memory climbs steadily until it is OOM-killed every few days. How do you find the leak?

    Reveal model answer

    First confirm it is a JS heap leak by tracking heapUsed versus RSS over time; growing RSS with a flat heap points at native memory or buffers. Then take heap snapshots after warm-up and again after sustained load (via --heapsnapshot-signal, v8.writeHeapSnapshot or the inspector), load them in Chrome DevTools, use the Comparison view to find object types whose count keeps growing, and follow their retainers back to the root. Snapshots block the event loop and need roughly as much memory again as the heap, so take them on an instance pulled out of rotation.

    WHAT A STRONG ANSWER COVERS

    • Distinguish heap growth from RSS/native growth.
    • Compare snapshots over time; look at deltas and retainer paths.
    • Typical culprits: unbounded caches, listeners added per request, global maps keyed by request, closures holding big objects.
    • Snapshots pause the process and roughly double memory.
    • Raising --max-old-space-size only delays the crash.

    RED FLAGS

    • Restarting on a schedule and calling it fixed.
    • Guessing at causes without a snapshot.

    LIKELY FOLLOW-UPS

    • What does a MaxListenersExceededWarning tell you?
    • How would you reproduce the leak locally?

    SOURCES

  12. Q12Mid

    What is prototype pollution, and what basic input-handling rules protect a Node.js API?

    Reveal model answer

    Prototype pollution is when attacker-controlled keys like `__proto__`, `constructor` or `prototype` reach a recursive merge or a dynamic property assignment and modify Object.prototype, changing behaviour for every object in the process and sometimes enabling auth bypass or crashes. Defend by validating every request against a schema that rejects unknown keys, avoiding naive deep merges, using Map or Object.create(null) for user-keyed data and Object.hasOwn for checks. More broadly: limit body sizes, use parameterised queries, avoid ReDoS-prone regexes, and set server timeouts.

    WHAT A STRONG ANSWER COVERS

    • Pollution comes through merges, path setters and query-string parsers with user-controlled keys.
    • Schema validation at the boundary with unknown keys rejected or stripped.
    • Map or prototype-less objects for dictionaries keyed by user input.
    • Body size limits, timeouts and safe regexes prevent cheap DoS.
    • Parameterised queries prevent injection; never build SQL from strings.

    RED FLAGS

    • Trusting TypeScript types as input validation.
    • Using deep-merge on raw request bodies.

    LIKELY FOLLOW-UPS

    • What does the --disable-proto flag do?
    • How do you keep dependencies from introducing these bugs?

    SOURCES