QuizCluster
Backend EngineeringBackend Engineer II to Staff Node.js Architect17 min read

Node.js Backend Interview Guide: Event Loop, Streams, APIs & Scaling

From libuv Internals to Production-Grade APIs, Worker Threads & Multi-Core Scaling

Marcus Chen
Staff Backend Engineer & Node.js Performance Consultant
13+ Years Building High-Throughput Node.js Platforms
Prep Timeline
5 to 7 Weeks
Format
Event Loop Internals, Streams, Concurrency, API Design & Scaling
Conversion
+74% Backend Offer Conversion
Node.js Backend Interview Guide: Event Loop, Streams, APIs & Scaling
Executive Summary & Key Takeaways

What You Must Master to Clear This Track

  • Internalize the libuv event loop's six phases and how the process.nextTick and Promise microtask queues drain completely between every phase transition.
  • Treat backpressure as a first-class concern: prefer stream.pipeline() and check the write() return value instead of chaining raw .pipe() calls with no error handling.
  • Reserve worker_threads and the libuv threadpool for genuinely CPU-bound work (crypto, compression, image processing); keep I/O-bound work on the main event loop with async APIs.
  • Design REST/GraphQL APIs around idempotency keys, structured error middleware, cursor-based pagination, and rate limiting from day one, not as a later retrofit.
  • Scale horizontally with the cluster module or PM2 behind a load balancer, and always implement graceful shutdown so rolling deploys never drop in-flight requests.
Structured Preparation Timeline

Step-by-Step Study Plan

Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.

Phase 1 (Weeks 1-2)

libuv Internals & Non-Blocking I/O Mastery

Event Loop, Microtasks & Async Fundamentals

The six event loop phases, process.nextTick vs Promise microtasks vs setImmediate/setTimeout, and how the libuv threadpool handles fs/crypto/dns work.

Key Milestones
  • Trace the exact execution order of a script mixing setTimeout, setImmediate, process.nextTick, and Promise.then.
  • Explain what blocks the event loop and how to measure event loop lag with perf_hooks.monitorEventLoopDelay.
  • Understand UV_THREADPOOL_SIZE and which core APIs (fs, crypto.pbkdf2, dns.lookup, zlib) actually use the libuv threadpool.
Recommended Actions
  • Rebuild the event loop diagram from memory: timers -> pending callbacks -> poll -> check -> close callbacks.
  • Profile a toy Express app with clinic.js or --prof to spot synchronous bottlenecks.
Phase 2 (Weeks 3-4)

Backpressure-Safe Streams, Cluster & Worker Threads

Streams, Buffers & CPU-Bound Scaling

Readable/Writable/Transform stream mechanics, backpressure signaling, the cluster module's shared-socket model, and worker_threads with SharedArrayBuffer.

Key Milestones
  • Implement a Transform stream and wire it through stream.pipeline() with correct error propagation.
  • Explain how cluster.fork() workers share a listening socket and how connections get distributed across them.
  • Offload a CPU-bound task (hashing, image resize, Fibonacci-style computation) to a worker_threads pool without blocking the main thread.
Recommended Actions
  • Never call worker.postMessage in a hot loop without batching; measure serialization overhead.
  • Benchmark highWaterMark tuning on a real file-processing pipeline and observe the 'drain' event firing.
Phase 3 (Weeks 5-6)

REST/GraphQL Hardening, Process Management & Zero-Downtime Deploys

Production APIs, Observability & Horizontal Scaling

Middleware pipelines, authentication, rate limiting, GraphQL N+1 mitigation with DataLoader, PM2 cluster mode, and graceful shutdown under SIGTERM.

Key Milestones
  • Design an Express or Fastify middleware chain with centralized error handling and input validation.
  • Implement graceful shutdown that drains in-flight requests, closes DB pools, and force-exits after a timeout.
  • Explain sticky sessions for WebSocket-heavy services and health-check based rolling deploys behind PM2 or Kubernetes.
Recommended Actions
  • Run mock system-design drills for 'design a rate limiter' and 'scale a webhook ingestion service to 10k RPS'.
  • Write out your own graceful shutdown handler from scratch without looking at a reference implementation.
Deep-Dive Architecture & Concepts

1. The libuv Event Loop: Phases, Microtasks & Non-Blocking I/O

Node.js is single-threaded for your JavaScript, but it delegates I/O to libuv, which cycles through a fixed sequence of phases. Interviewers use this topic to separate candidates who memorized 'Node is async' from those who understand exactly when each callback fires.

Timers Phase

Executes setTimeout/setInterval callbacks whose threshold has elapsed, ordered by scheduled expiry, not by insertion order.

Poll Phase

Retrieves new I/O events from the OS (epoll on Linux, kqueue on macOS, IOCP on Windows) and executes their callbacks; the loop can block here waiting for I/O if no timers or setImmediate calls are pending.

Check Phase (setImmediate)

Runs immediately after the poll phase completes for the current iteration, which is why setImmediate fires before setTimeout(fn, 0) when scheduled inside an I/O callback.

Microtask Queues (nextTick & Promises)

process.nextTick() and resolved Promise callbacks are NOT a loop phase; they drain completely (nextTick queue first, then the Promise microtask queue) after every callback and between every phase transition.

The libuv Event Loop Phase Cycle

One tick of the loop; microtasks (nextTick, then Promises) fully drain between each phase and after each individual callback.

1
Timers
Run expired setTimeout/setInterval callbacks in expiry order; microtask queues drain after each one.
2
Pending Callbacks
Execute I/O callbacks deferred from the prior iteration, such as certain TCP errors (e.g. ECONNREFUSED).
3
Poll
Fetch new I/O events from the OS and run their callbacks; blocks here if no timers/setImmediate are due, keeping CPU idle.
4
Check
Run all setImmediate() callbacks queued during this iteration, always immediately after poll finishes.
5
Close Callbacks
Run 'close' event handlers (e.g. socket.on('close')) before the loop wraps back to Timers.
Microtask vs Macrotask Ordering
javascript
console.log('start');
  
  setTimeout(() => console.log('timeout'), 0);
  
  setImmediate(() => console.log('immediate'));
  
  process.nextTick(() => console.log('nextTick'));
  
  Promise.resolve().then(() => console.log('promise'));
  
  console.log('end');
  
  // Deterministic part of the output:
  // start
  // end
  // nextTick   <- microtask queue #1 always drains first
  // promise    <- microtask queue #2 drains next
  // Then either 'timeout' or 'immediate' first (order is NOT guaranteed
  // in the main module), but inside an I/O callback 'immediate' ALWAYS
  // fires before a 0ms 'timeout' because Check runs right after Poll.
Why it matters: process.nextTick and Promise microtasks always drain before the loop advances to the next phase, so they beat any timer or setImmediate callback scheduled at the same point in the script.
Interviewer Insights & Pro Tips
  • When asked 'what blocks the event loop', give concrete examples: synchronous JSON.parse on huge payloads, unbounded regex backtracking, and crypto.pbkdf2Sync in a request handler.
  • Mention perf_hooks.monitorEventLoopDelay() as the production tool for measuring event loop lag, not just anecdotal 'the server feels slow'.
Red Flags & Common Pitfalls
  • Assuming setTimeout(fn, 0) always fires before setImmediate — the order is only guaranteed inside an I/O callback, not in the top-level module.
  • Forgetting that a recursive process.nextTick() call can starve the event loop entirely, since the nextTick queue must fully empty before the loop proceeds.
Deep-Dive Architecture & Concepts

2. Streams & Backpressure: Readable, Writable & Transform

Streams are Node's answer to processing data too large to fit in memory, but they're also one of the most common places production incidents happen when backpressure is ignored.

Paused vs Flowing Mode

A Readable stream starts paused; attaching a 'data' listener or calling .pipe()/.resume() switches it to flowing mode, where chunks are pushed as fast as they're produced.

Backpressure Signal

writable.write(chunk) returns false once internal buffered data exceeds highWaterMark; well-behaved producers pause until the stream emits 'drain'.

stream.pipeline() over .pipe()

pipeline() automatically forwards errors, destroys all streams on failure, and calls a completion callback — raw .pipe() chains leak file descriptors on unhandled errors.

Transform Streams

A Duplex stream where output is computed from input (e.g. gzip, CSV parsing); _transform(chunk, enc, callback) must call callback exactly once per chunk to respect flow control.

Backpressure-Safe Pipeline with a Custom Transform Stream
javascript
const { pipeline, Transform } = require('node:stream');
  const fs = require('node:fs');
  const zlib = require('node:zlib');
  
  class UpperCaseTransform extends Transform {
    _transform(chunk, encoding, callback) {
      // callback() signals "ready for the next chunk" — this is the
      // backpressure contract: never call push() after an unhandled error,
      // and never call callback() more than once per chunk.
      this.push(chunk.toString().toUpperCase());
      callback();
    }
  }
  
  pipeline(
    fs.createReadStream('input.txt'),
    new UpperCaseTransform(),
    zlib.createGzip(),
    fs.createWriteStream('output.txt.gz'),
    (err) => {
      if (err) {
        console.error('Pipeline failed:', err);
        process.exitCode = 1;
      } else {
        console.log('Pipeline succeeded — backpressure handled end-to-end');
      }
    }
  );
Why it matters: pipeline() propagates backpressure across all four streams automatically and guarantees every stream is destroyed if any one of them errors, avoiding the dangling file-descriptor leaks common with chained .pipe() calls.
Interviewer Insights & Pro Tips
  • In interviews, define backpressure precisely: it's the mechanism preventing a fast producer from overwhelming a slow consumer's memory buffer.
  • Bring up object mode streams when discussing processing structured records (e.g. parsed JSON rows) instead of raw Buffers/strings.
Red Flags & Common Pitfalls
  • Ignoring the boolean return value of writable.write() and continuing to write anyway, causing unbounded memory growth under load.
  • Chaining readable.pipe(transform).pipe(writable) without any 'error' listeners, so a mid-pipeline error is silently swallowed and the process hangs.
Deep-Dive Architecture & Concepts

3. Scaling CPU Work: The Cluster Module & Worker Threads

A single Node process runs JavaScript on one thread. Real production systems need two different tools depending on the bottleneck: the cluster module to use all CPU cores for I/O-bound throughput, and worker_threads to offload genuinely CPU-bound computation.

Cluster Module Architecture

A primary process forks N worker processes (typically os.cpus().length); on Linux/macOS the OS load-balances new connections across workers sharing the same listening socket (round-robin on most platforms via SCHED_RR).

worker_threads for CPU-Bound Work

Unlike cluster's separate processes, worker_threads run in the same process with isolated V8 heaps, communicating via structured-clone message passing or shared memory through SharedArrayBuffer + Atomics.

The libuv Threadpool Is Not worker_threads

fs, crypto.pbkdf2/scrypt, zlib, and dns.lookup already run on libuv's internal threadpool (default size 4, tunable via UV_THREADPOOL_SIZE) — you don't need worker_threads for these.

child_process vs worker_threads

child_process spawns a fully separate OS process (heavier, isolated memory, own event loop) — better for running untrusted or non-Node executables; worker_threads is lighter-weight for pure computation within the same app.

Offloading CPU-Bound Work to a worker_threads Pool
javascript
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
  
  function runInWorker(data) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData: data });
      worker.once('message', resolve);
      worker.once('error', reject);
      worker.once('exit', (code) => {
        if (code !== 0) reject(new Error('Worker stopped with exit code ' + code));
      });
    });
  }
  
  if (isMainThread) {
    // Main thread stays free to keep serving other requests while this runs.
    runInWorker({ n: 42 }).then((result) => {
      console.log('CPU-bound result computed off the main event loop:', result);
    });
  } else {
    // Runs on a separate V8 isolate — never blocks the main thread's event loop.
    function fib(n) {
      return n < 2 ? n : fib(n - 1) + fib(n - 2);
    }
    parentPort.postMessage(fib(workerData.n));
  }
Why it matters: Spawning the worker with __filename lets the same module act as both the orchestrator and the worker entry point; the CPU-heavy fib() computation never touches the main thread's event loop, so other requests keep being served concurrently.
Interviewer Insights & Pro Tips
  • State the rule of thumb explicitly: I/O-bound work stays on the main thread using async APIs; only genuinely CPU-bound work (image resizing, hashing, large JSON transforms, ML inference) belongs in worker_threads.
  • Mention Piscina or a hand-rolled worker pool to avoid the cost of spinning up a new thread per request.
Red Flags & Common Pitfalls
  • Reaching for worker_threads to 'fix' a slow database query — that's an I/O latency problem, not a CPU problem, and threads won't help.
  • Passing huge objects to postMessage() without realizing the default structured-clone cost; SharedArrayBuffer avoids the copy for numeric data that needs frequent cross-thread access.
Deep-Dive Architecture & Concepts

4. Production APIs & Horizontal Scaling: Middleware, PM2 & Graceful Shutdown

Shipping an API that survives real traffic means getting the middleware pipeline, error handling, and process lifecycle right — this is where 'it works on my machine' backend code gets separated from production-grade backend code.

Middleware Pipeline Order

Express/Fastify execute middleware in registration order: request parsing -> auth -> rate limiting -> route handler -> centralized error-handling middleware (4-arg signature in Express) last.

GraphQL N+1 & DataLoader

Resolving a list field per-parent triggers one query per item; DataLoader batches and caches those lookups within a single request tick, collapsing N queries into 1.

PM2 Cluster Mode

pm2 start app.js -i max forks one process per CPU core (built on the cluster module under the hood) and adds zero-downtime reloads via pm2 reload.

Graceful Shutdown Contract

On SIGTERM: stop accepting new connections, let in-flight requests finish, close DB/Redis pools, then exit — with a hard timeout fallback so a stuck deploy doesn't hang forever.

Graceful Shutdown Handler for a PM2/Kubernetes-Managed Node Service
javascript
const server = app.listen(port, () => console.log('Listening on ' + port));
  
  let shuttingDown = false;
  
  async function gracefulShutdown(signal) {
    if (shuttingDown) return;
    shuttingDown = true;
    console.log(signal + ' received: draining connections');
  
    server.close(async () => {
      try {
        await dbPool.end();       // close DB connection pool cleanly
        await redisClient.quit(); // close cache connections cleanly
        console.log('Cleanup complete, exiting');
        process.exit(0);
      } catch (err) {
        console.error('Error during shutdown', err);
        process.exit(1);
      }
    });
  
    // Force-exit if connections don't drain in time (e.g. a stuck socket)
    setTimeout(() => process.exit(1), 10_000).unref();
  }
  
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
Why it matters: server.close() stops accepting new connections while letting existing requests complete; the unref()'d timeout guarantees the process still exits even if a socket never drains, which matters for Kubernetes' pod termination grace period.
Interviewer Insights & Pro Tips
  • Bring up sticky sessions explicitly when discussing WebSocket or Socket.IO services behind a cluster/PM2/load balancer, since a client must keep hitting the same worker for stateful connections.
  • Mention readiness vs liveness probes: readiness should fail fast during shutdown so the load balancer stops routing new traffic before the process actually exits.
Red Flags & Common Pitfalls
  • Calling process.exit() immediately on SIGTERM without draining in-flight requests, causing dropped responses on every rolling deploy.
  • Running PM2 cluster mode and a hand-rolled cluster.fork() setup at the same time, silently doubling the number of worker processes per core.
Real-World Example

Scaling a Fintech Webhook Ingestion Service From 200 to 12,000 RPS

A payments team's Node.js/Express service ingested webhook events from card processors, verifying an HMAC signature and writing each event to Postgres before acknowledging. During a partner's Black Friday traffic spike, the service fell badly behind, timing out webhooks and triggering costly retry storms from the upstream processor.

  • 1Profiled the service under load with clinic.js and perf_hooks.monitorEventLoopDelay, finding synchronous HMAC verification (crypto.createHmac used correctly, but a heavy custom JSON schema validator run synchronously) was blocking the event loop for 40-80ms per request.
  • 2Moved the CPU-heavy schema validation into a small worker_threads pool sized to available cores, keeping HMAC verification (already fast) on the main thread.
  • 3Replaced ad-hoc .pipe() chains used for archiving raw payloads to S3 with stream.pipeline(), fixing a slow file-descriptor leak that had been causing periodic restarts.
  • 4Deployed the service under PM2 in cluster mode across all 8 cores per node, with a graceful shutdown handler draining in-flight requests on SIGTERM before deploys.
  • 5Added a Redis-backed token-bucket rate limiter and pushed downstream database writes through a BullMQ queue so ingestion accepted webhooks even when Postgres was briefly saturated.
  • 6Added readiness probes that failed fast during both startup and shutdown so the load balancer never routed traffic to a pod that wasn't truly ready.
Outcome: p99 latency dropped from 2.4s to 180ms, and the service scaled from roughly 200 RPS per instance to over 12,000 RPS across an 8-node PM2 cluster, with zero webhooks dropped during subsequent rolling deploys.
Real-World Interview Questions

Top Must-Know Interview Questions & Model Answers

Event Loop & AsyncMust-Know

Q1: Walk through the six phases of the Node.js event loop in order.

Executive Answer:Timers, pending callbacks, idle/prepare (internal), poll, check, and close callbacks, with microtask queues draining between every phase and every individual callback.
Deep Dive Analysis:
  • Timers phase runs expired setTimeout/setInterval callbacks ordered by expiry time, not insertion order.
  • Poll phase is where the loop can block waiting for new I/O if nothing else is scheduled; it also runs I/O callbacks that have already fired.
  • Check phase runs setImmediate callbacks immediately after poll finishes for that iteration; close callbacks phase runs handlers like socket.on('close') last.
Interviewer Takeaway: Draw the loop as a ring with microtask drains happening at every arrow between phases, not as a single flat queue.
Event Loop & AsyncHard

Q2: What is the difference between process.nextTick(), Promise microtasks, and setImmediate()?

Executive Answer:nextTick and Promise callbacks are microtasks that drain fully before the loop moves anywhere; setImmediate is a macrotask tied to the Check phase, running after the current Poll phase completes.
Deep Dive Analysis:
  • The nextTick queue is processed before the Promise microtask queue whenever both have pending callbacks.
  • Microtasks run after every single callback completes, not just between phases — a recursive nextTick call can starve the entire event loop.
  • setImmediate is guaranteed to run before a 0ms setTimeout only when both are scheduled from within an I/O callback.
Interviewer Takeaway: Microtasks (nextTick, Promise) always jump the queue ahead of any timer or setImmediate callback.
Event Loop & AsyncMust-Know

Q3: Why is Node.js described as 'single-threaded' when it clearly uses multiple threads under the hood?

Executive Answer:Your JavaScript callback code runs on exactly one thread, but libuv maintains a background threadpool (default 4 threads) for blocking OS operations like file I/O and certain crypto functions.
Deep Dive Analysis:
  • Network I/O (sockets, HTTP) uses OS-level async mechanisms (epoll/kqueue/IOCP) and doesn't need the threadpool at all.
  • fs operations, crypto.pbkdf2/scrypt, zlib compression, and dns.lookup are dispatched to the libuv threadpool because the underlying syscalls are blocking.
  • V8 itself also runs GC and JIT compilation on auxiliary threads, separate from your JS execution thread.
Interviewer Takeaway: 'Single-threaded' refers to your JS execution context, not to the Node.js runtime as a whole.
Event Loop & AsyncMedium

Q4: What are common causes of event loop blocking in a production API, and how would you detect it?

Executive Answer:Synchronous CPU-heavy operations (large JSON.parse, unbounded regex, tight loops, *Sync fs/crypto calls) block the single JS thread; detect it with perf_hooks.monitorEventLoopDelay() or APM event-loop-lag metrics.
Deep Dive Analysis:
  • A single request doing JSON.parse on a 50MB payload synchronously stalls every other concurrent request until it finishes.
  • Catastrophic regex backtracking (ReDoS) on user input is a classic blocking bug that also doubles as a security vulnerability.
  • Tools like clinic.js doctor or 0x flamegraphs pinpoint exactly which function is holding the thread.
Interviewer Takeaway: Any synchronous CPU-bound operation in a request handler blocks every other in-flight request — move it to a worker or make it async.
Event Loop & AsyncMedium

Q5: What is the libuv threadpool and how do you tune it?

Executive Answer:A fixed-size pool of background OS threads (default size 4) that Node uses for blocking operations without OS-native async support; its size is controlled by the UV_THREADPOOL_SIZE environment variable (max 1024).
Deep Dive Analysis:
  • fs (excluding a few that use native async APIs), DNS lookups via dns.lookup, and some crypto/zlib functions are dispatched here.
  • If you run many concurrent password hashes (bcrypt/scrypt) with only 4 threadpool slots, requests queue up waiting for a free thread even though your event loop itself is idle.
  • UV_THREADPOOL_SIZE must be set before the process starts (it's read once at startup) and applies process-wide, not per-request.
Interviewer Takeaway: If concurrent crypto/fs-heavy requests feel throttled despite low CPU usage, suspect threadpool starvation, not the event loop.
Event Loop & AsyncMedium

Q6: What is the difference between a microtask queue and a macrotask (event loop phase) queue?

Executive Answer:Microtasks (nextTick, Promise callbacks) are drained to completion — including any new microtasks they schedule — before the loop can proceed; macrotasks (timers, I/O, setImmediate) are each tied to a specific loop phase and only one phase's worth runs per iteration.
Deep Dive Analysis:
  • Because microtasks fully drain before the next macrotask, an infinite chain of Promise.then() calls will starve timers and I/O indefinitely.
  • This distinction is why 'async/await heavy' code can still starve the event loop if each awaited function schedules more microtasks synchronously without ever yielding to I/O.
Interviewer Takeaway: Microtasks have priority over macrotasks at every single opportunity, which is powerful but also a starvation risk.
Streams & BackpressureMedium

Q7: Explain the difference between paused (non-flowing) and flowing mode in a Readable stream.

Executive Answer:In paused mode you must explicitly call .read() to pull a chunk; in flowing mode (triggered by a 'data' listener, .pipe(), or .resume()) chunks are pushed to you automatically as fast as they're produced.
Deep Dive Analysis:
  • Every Readable stream starts in paused mode; switching to flowing and back to paused is possible but rarely done manually in application code.
  • Flowing mode without any backpressure-aware consumer is exactly how memory blows up when reading a huge file faster than it can be processed downstream.
Interviewer Takeaway: Prefer .pipe()/pipeline() over manual 'data' listeners — they manage the paused/flowing transitions and backpressure for you.
Streams & BackpressureMust-Know

Q8: What exactly is backpressure in Node.js streams, and how is it signaled?

Executive Answer:Backpressure is the mechanism that prevents a fast producer from overwhelming a slow consumer's memory buffer; writable.write(chunk) returns false once buffered data exceeds highWaterMark, and the stream later emits 'drain' when it's safe to resume.
Deep Dive Analysis:
  • Ignoring the false return value and continuing to call write() anyway causes internal buffers to grow unbounded, eventually crashing the process with an out-of-memory error.
  • pipe() and pipeline() handle this automatically by pausing the source stream until 'drain' fires on the destination.
Interviewer Takeaway: Backpressure is a contract, not a suggestion — always check write()'s return value or delegate to pipeline().
Streams & BackpressureMedium

Q9: What's the difference between a Duplex stream and a Transform stream?

Executive Answer:Both implement Readable and Writable sides, but a Duplex stream's input and output are independent (e.g. a TCP socket), while a Transform stream's output is derived directly from its input (e.g. gzip, a CSV parser).
Deep Dive Analysis:
  • Transform streams implement _transform(chunk, encoding, callback) instead of separate _read/_write, and can also implement _flush() for any trailing output.
  • A net.Socket is the canonical Duplex example: what you read from it has nothing to do with what you write to it.
Interviewer Takeaway: If output is a function of input, reach for Transform; if the two sides are unrelated, it's a plain Duplex.
Streams & BackpressureHard

Q10: Why should you prefer stream.pipeline() over chaining multiple .pipe() calls?

Executive Answer:pipeline() forwards errors from any stream in the chain, guarantees every stream is properly destroyed on failure, and invokes a single completion callback — plain .pipe() chains silently leak file descriptors and hang on unhandled errors.
Deep Dive Analysis:
  • With raw .pipe(), an error on any stream except the last one is not automatically propagated and must be manually listened for on each stream.
  • pipeline() (and its Promise-based util.promisify or stream/promises variant) is now the documented, recommended way to compose stream chains since Node 10.
Interviewer Takeaway: pipeline() is to streams what try/finally is to resource cleanup — automatic teardown on any failure path.
Streams & BackpressureMedium

Q11: How does tuning highWaterMark affect a stream's memory and throughput trade-off?

Executive Answer:highWaterMark sets the internal buffer size threshold before backpressure kicks in; a higher value increases throughput and memory use, a lower value reduces memory footprint at the cost of more frequent pause/drain cycling.
Deep Dive Analysis:
  • Default is 16KB for byte streams and 16 objects for object-mode streams.
  • For large-file processing pipelines under memory pressure (e.g. containers with tight limits), lowering highWaterMark trades some throughput for predictable memory ceilings.
Interviewer Takeaway: highWaterMark is a buffering knob, not a hard cap — treat it as a throughput/memory trade-off you tune empirically.
Streams & BackpressureMedium

Q12: What is object mode in Node.js streams and when would you use it?

Executive Answer:Object mode lets a stream push arbitrary JavaScript values (not just Buffers/strings), which is useful for pipelines that operate on structured records like parsed JSON rows or database documents.
Deep Dive Analysis:
  • Enabled by passing { objectMode: true } to the stream constructor; highWaterMark then counts objects instead of bytes.
  • Common in ETL-style pipelines: a Transform stream parses raw CSV bytes into row objects, and downstream Transforms operate purely on those objects.
Interviewer Takeaway: Reach for object mode the moment your pipeline stops thinking in bytes and starts thinking in records.
Cluster & Worker ThreadsMust-Know

Q13: Explain the architecture of the Node.js cluster module.

Executive Answer:A primary process calls cluster.fork() to spawn N worker processes, each running the full application; the primary shares its listening socket with all workers so incoming connections are distributed among them.
Deep Dive Analysis:
  • On most platforms the OS/kernel or Node's internal round-robin scheduler decides which worker handles each new connection.
  • Workers are separate OS processes with independent memory and event loops — a crash in one worker doesn't take down the others.
  • The primary can listen for 'exit' events and re-fork a replacement worker for self-healing.
Interviewer Takeaway: cluster gives you multi-core throughput for I/O-bound workloads by running N independent copies of your app, not by parallelizing a single request.
Cluster & Worker ThreadsHard

Q14: When would you choose worker_threads over the cluster module, or vice versa?

Executive Answer:Use cluster to scale I/O-bound request throughput across CPU cores by running independent processes; use worker_threads to offload a specific CPU-bound computation without duplicating your entire app's memory and connections.
Deep Dive Analysis:
  • cluster workers are full processes (heavier, isolated memory, separate DB connection pools per worker) — great for horizontal request throughput.
  • worker_threads share the process but have isolated V8 heaps, communicating via message passing or SharedArrayBuffer — ideal for a single hot CPU-bound function.
  • You can combine both: cluster for request distribution across cores, and a worker_threads pool inside each cluster worker for occasional heavy computation.
Interviewer Takeaway: cluster scales 'how many requests', worker_threads scales 'how much CPU work per request' — they solve different bottlenecks.
Cluster & Worker ThreadsHard

Q15: How do worker_threads share memory efficiently, and what problem does SharedArrayBuffer solve?

Executive Answer:By default, postMessage() structured-clones data between threads, which copies it; SharedArrayBuffer instead gives multiple threads a view onto the same underlying memory, avoiding copy overhead for large numeric datasets, paired with Atomics for safe concurrent access.
Deep Dive Analysis:
  • Structured cloning is fine for small messages but becomes a bottleneck when passing large buffers or arrays repeatedly.
  • Atomics.wait/notify provide low-level synchronization primitives so multiple threads can safely read/write shared memory without races.
Interviewer Takeaway: Reach for SharedArrayBuffer only when message-passing overhead is measured and proven to be the bottleneck — it adds real complexity.
Cluster & Worker ThreadsMedium

Q16: How does the cluster module distribute incoming connections across workers?

Executive Answer:On Linux/macOS, Node's cluster module uses a round-robin scheduler by default (cluster.schedulingPolicy = SCHED_RR) to hand off new connections to workers; on Windows, the OS itself load-balances via the shared socket.
Deep Dive Analysis:
  • Round-robin can be switched off in favor of SCHED_NONE, letting the OS decide, which sometimes causes uneven load in practice.
  • Long-lived connections (like WebSockets) complicate this model since a worker restart drops that worker's active connections.
Interviewer Takeaway: Round-robin distribution assumes short-lived, stateless requests — persistent connections need sticky-session or gateway-level handling instead.
Cluster & Worker ThreadsHard

Q17: How would you achieve a zero-downtime restart of a clustered Node.js application?

Executive Answer:Restart workers one at a time: fork a new worker, wait for it to become ready, then gracefully shut down an old worker — never killing all workers simultaneously — which is exactly what PM2's 'reload' and Node's cluster 'disconnect' pattern implement.
Deep Dive Analysis:
  • The primary listens for a signal, forks a replacement worker, waits for the 'listening' event, then calls worker.disconnect() on one old worker at a time.
  • Each outgoing worker should still finish in-flight requests via graceful shutdown before exiting.
Interviewer Takeaway: Zero-downtime restarts are rolling restarts at the worker level, combined with per-worker graceful shutdown.
Cluster & Worker ThreadsMedium

Q18: What's the difference between child_process, cluster, and worker_threads?

Executive Answer:child_process spawns an arbitrary separate process (any executable, not just Node); cluster is a specialized wrapper around child_process for running multiple copies of the same Node server sharing a socket; worker_threads runs isolated JS execution contexts within the same process.
Deep Dive Analysis:
  • child_process.spawn/exec/fork all create OS-level processes with their own memory and event loop; fork() specifically is for spawning other Node scripts with an IPC channel.
  • worker_threads is the lightest-weight option since it avoids full process startup cost, making it better suited to short-lived CPU-bound tasks.
Interviewer Takeaway: Think of it as a weight spectrum: child_process (heaviest, most isolated) > cluster (process-per-core) > worker_threads (lightest, same process).
REST & GraphQL APIsMust-Know

Q19: Design the middleware pipeline for a production Express or Fastify API. What order do things run in?

Executive Answer:Request parsing/logging first, then authentication, then rate limiting/validation, then the route handler, and finally a centralized error-handling middleware that catches anything thrown or passed to next(err).
Deep Dive Analysis:
  • Express executes middleware strictly in registration order; an error-handling middleware must have exactly 4 parameters (err, req, res, next) to be recognized as such.
  • Fastify uses a similar hooks model (onRequest, preHandler, etc.) but validates/serializes against JSON Schema at each route for better performance.
Interviewer Takeaway: Order matters: auth before business logic, and one centralized error handler at the end instead of scattered try/catch blocks in every route.
REST & GraphQL APIsHard

Q20: How do you solve the N+1 query problem in a GraphQL API?

Executive Answer:Use a batching/caching layer like DataLoader, which collects all the individual key lookups requested during a single event loop tick and resolves them with one batched query instead of one query per item.
Deep Dive Analysis:
  • Without DataLoader, resolving a 'friends' field for 100 users triggers 100 separate database round-trips, one per user.
  • DataLoader keys its cache per-request (a fresh instance per GraphQL request) to avoid stale cross-request data leaking between users.
Interviewer Takeaway: Any GraphQL list-of-objects resolver that queries per-item is a strong signal to introduce batching via DataLoader.
REST & GraphQL APIsMedium

Q21: What strategies exist for API versioning, and what are the trade-offs?

Executive Answer:URI versioning (/v1/users), header-based versioning (Accept: application/vnd.api.v2+json), and query-parameter versioning are the three common approaches, each trading discoverability against cache-friendliness and REST purity.
Deep Dive Analysis:
  • URI versioning is the most discoverable and cache-friendly but clutters the URL space and encourages long-lived old versions.
  • Header-based versioning keeps URLs clean but is harder to test manually and less visible to API consumers browsing docs.
Interviewer Takeaway: Pick URI versioning by default for public APIs — it's the most operationally simple to route, cache, and deprecate.
REST & GraphQL APIsMedium

Q22: How would you implement a rate limiter middleware for a Node.js API?

Executive Answer:Use a token-bucket or sliding-window counter backed by Redis (for multi-instance consistency), rejecting requests with 429 Too Many Requests once a client's key exceeds its allotted quota within the window.
Deep Dive Analysis:
  • In-memory rate limiting only works correctly on a single instance; behind a cluster or multiple horizontally scaled pods it must be centralized in Redis or similar.
  • Token bucket allows short bursts up to the bucket size while enforcing a steady average rate, which fits bursty client traffic better than a strict fixed window.
Interviewer Takeaway: Any rate limiter that only lives in process memory silently breaks the moment you scale to more than one instance.
REST & GraphQL APIsMedium

Q23: What performance advantages does Fastify offer over Express, and when does it matter?

Executive Answer:Fastify uses JSON Schema-based compiled serialization and a more optimized routing tree, yielding measurably higher requests-per-second than Express, which matters most for high-throughput, latency-sensitive services.
Deep Dive Analysis:
  • Schema-based response serialization in Fastify is compiled ahead of time into a fast JSON stringify function instead of relying on the generic JSON.stringify.
  • For most CRUD APIs with modest traffic the difference is immaterial; it becomes relevant at very high RPS or when serialization dominates request latency.
Interviewer Takeaway: Don't over-index on Fastify vs Express benchmarks for a low-traffic service — pick based on ecosystem/team familiarity unless you have a measured bottleneck.
REST & GraphQL APIsHard

Q24: How would you design JWT authentication with refresh token rotation for an API?

Executive Answer:Issue a short-lived access token (e.g. 15 min) for API calls and a longer-lived refresh token stored in an HttpOnly cookie; each refresh call issues a brand-new refresh token and invalidates the old one to detect token theft.
Deep Dive Analysis:
  • Rotating refresh tokens means reusing an already-used refresh token is a strong signal of theft, letting you revoke the whole session family.
  • Access tokens should never be stored in localStorage in a way exposed to XSS; HttpOnly, SameSite cookies mitigate token exfiltration.
Interviewer Takeaway: Rotation + reuse detection is what turns a stolen refresh token into a detectable, revocable incident instead of a silent long-term compromise.
REST & GraphQL APIsMedium

Q25: How do you make a POST endpoint idempotent to safely handle client retries?

Executive Answer:Require an Idempotency-Key header from the client; store the first response keyed by that value (e.g. in Redis with a TTL) and return the cached response for any duplicate request with the same key instead of re-executing the side effect.
Deep Dive Analysis:
  • This is critical for payment and order-creation endpoints where a network retry after a timeout must never double-charge a customer.
  • The key should be scoped per-user/per-resource to avoid collisions between unrelated clients.
Interviewer Takeaway: Idempotency keys turn 'at-least-once' network delivery into effectively 'exactly-once' side effects at the API boundary.
Scaling & Process ManagementMedium

Q26: What's the difference between PM2 cluster mode and manually using the Node cluster module?

Executive Answer:PM2 cluster mode wraps the same underlying cluster module but adds operational tooling on top: process monitoring, automatic restarts on crash, log aggregation, and zero-downtime reload orchestration via a single CLI command.
Deep Dive Analysis:
  • pm2 start app.js -i max forks one worker per CPU core automatically, equivalent to manually calling cluster.fork() os.cpus().length times.
  • PM2 additionally exposes metrics (CPU/memory per worker) and a process list, which a hand-rolled cluster setup doesn't give you for free.
Interviewer Takeaway: PM2 is an operations layer on top of cluster, not a replacement for understanding what cluster actually does underneath.
Scaling & Process ManagementMust-Know

Q27: Walk through a correct graceful shutdown sequence for a Node.js service running under Kubernetes.

Executive Answer:On SIGTERM, flip the readiness probe to failing immediately, stop accepting new connections via server.close(), let in-flight requests finish, close DB/cache connection pools, then exit — with a hard timeout that force-exits if draining takes too long.
Deep Dive Analysis:
  • Kubernetes sends SIGTERM and waits for terminationGracePeriodSeconds (default 30s) before sending SIGKILL, so your drain timeout must be safely shorter than that.
  • Failing readiness before closing the server gives the load balancer/kube-proxy time to stop routing new traffic to the pod before it actually stops accepting connections.
Interviewer Takeaway: Graceful shutdown is a sequence, not a single process.exit() call — readiness fails first, connections drain second, cleanup happens third.
Scaling & Process ManagementHard

Q28: How do you handle WebSocket connections in a horizontally scaled, load-balanced Node.js deployment?

Executive Answer:Either enable sticky sessions at the load balancer so a client always reconnects to the same instance, or use a shared pub/sub backplane (like Redis adapter for Socket.IO) so any instance can broadcast to clients connected on any other instance.
Deep Dive Analysis:
  • Sticky sessions are simpler but reduce the effectiveness of load balancing and complicate rolling deploys, since restarting one instance disconnects all its sockets.
  • A Redis (or similar) pub/sub backplane decouples 'which instance holds the socket' from 'which instance needs to send a message', enabling true stateless horizontal scaling.
Interviewer Takeaway: For anything beyond a small deployment, a pub/sub backplane scales WebSockets far better than sticky sessions alone.
Scaling & Process ManagementMedium

Q29: What's the difference between horizontal and vertical scaling for a Node.js API, and why is horizontal generally preferred?

Executive Answer:Vertical scaling adds more CPU/RAM to a single instance (limited by hardware ceilings and a single point of failure); horizontal scaling adds more instances behind a load balancer, improving both capacity and fault tolerance.
Deep Dive Analysis:
  • Because a single Node process is bound mostly by one CPU core for JS execution, vertical scaling past a certain point yields diminishing returns unless paired with cluster/worker_threads to use the extra cores.
  • Horizontal scaling also enables rolling deploys and graceful degradation — losing one of ten instances is far less impactful than losing your only vertically-scaled instance.
Interviewer Takeaway: Node's single-threaded execution model makes horizontal scaling (more processes) the default answer, with vertical scaling only helping insofar as it allows more cluster/worker processes.
Scaling & Process ManagementMedium

Q30: What should a Node.js service's health check endpoints actually verify?

Executive Answer:A liveness probe should only confirm the process is running and not deadlocked; a readiness probe should verify the service can actually serve traffic, including live checks against critical dependencies like the database and cache.
Deep Dive Analysis:
  • Conflating the two causes cascading failures: if liveness checks the database and the DB has a blip, Kubernetes kills and restarts otherwise-healthy pods.
  • Readiness should fail during startup (before DB pool is warmed) and during graceful shutdown (once draining begins), so traffic is only ever routed to pods truly able to serve it.
Interviewer Takeaway: Liveness answers 'should this process be restarted?'; readiness answers 'should traffic be sent here right now?' — never merge the two.
Common Mistakes

Mistakes That Sink Otherwise Strong Candidates

Blocking the event loop with synchronous CPU work inside a request handler.

Why it happens: Operations like JSON.parse on huge payloads, *Sync fs/crypto calls, or unbounded regex feel 'simple' and their cost is invisible until concurrent load reveals it.

The fix: Profile with perf_hooks.monitorEventLoopDelay or clinic.js, and move genuinely CPU-bound work to a worker_threads pool while keeping I/O on async APIs.

Ignoring the return value of writable.write() and never respecting backpressure.

Why it happens: write() still 'works' when it returns false, so the bug only manifests as slow, hard-to-reproduce memory growth under real production load.

The fix: Check the boolean return value and pause the producer until 'drain' fires, or simply use stream.pipeline() which manages this automatically.

Chaining .pipe() calls with no error listeners on any stream in the chain.

Why it happens: Happy-path testing rarely triggers stream errors, so the missing error handling goes unnoticed until a real malformed file or network blip hangs the process.

The fix: Use stream.pipeline() (or its Promise-based variant), which forwards errors from any stream and guarantees proper cleanup on failure.

Reaching for worker_threads to fix a slow database query or API call.

Why it happens: Threads feel like a generic 'go faster' hammer, but a slow query is an I/O latency problem, and moving it to a worker thread doesn't reduce the wait.

The fix: Diagnose whether the bottleneck is CPU-bound (worker_threads helps) or I/O-bound (needs query optimization, caching, or connection pool tuning instead).

Leaving UV_THREADPOOL_SIZE at its default of 4 for a crypto- or fs-heavy workload.

Why it happens: The threadpool is invisible in code, so teams don't realize concurrent bcrypt/scrypt hashing or heavy file I/O is queuing behind only 4 background threads.

The fix: Set UV_THREADPOOL_SIZE (e.g. to the core count) before process startup, and benchmark to confirm the change actually relieves the bottleneck.

Not handling unhandledRejection and uncaughtException at the process level.

Why it happens: Individual try/catch blocks feel like sufficient coverage, but a missed .catch() on a fire-and-forget Promise crashes silently or leaves the process in an undefined state.

The fix: Register process-level handlers to log the error with full context and perform a controlled shutdown/restart rather than letting the process hang or crash uncontrolled.

Calling process.exit() immediately on SIGTERM without draining in-flight requests.

Why it happens: It looks like the 'simple' way to handle a shutdown signal, and it works fine locally where there's no real traffic to interrupt.

The fix: Implement a graceful shutdown sequence: stop accepting new connections, let existing requests finish, close DB/cache pools, then exit, backed by a hard timeout.

Running both PM2 cluster mode and manual cluster.fork() logic in the same app.

Why it happens: A team adopts PM2 for operational tooling but forgets the app already forks its own workers internally, silently multiplying the process count per core.

The fix: Pick exactly one layer to own process forking — either PM2's -i max flag or your own cluster module code, never both.

Storing rate-limit counters only in local process memory in a horizontally scaled deployment.

Why it happens: In-memory counters are the fastest thing to prototype and pass local testing on a single instance.

The fix: Back the rate limiter with Redis (or another shared store) so limits are enforced consistently across every instance behind the load balancer.

Treating sticky sessions as unnecessary because 'the app is stateless'.

Why it happens: REST endpoints genuinely are stateless, but WebSocket/Socket.IO connections are inherently long-lived and tied to whichever instance accepted them.

The fix: Either configure sticky sessions at the load balancer for WebSocket traffic, or adopt a Redis pub/sub backplane so any instance can reach any connected client.

Cheat Sheet

Quick-Reference Cheat Sheet

Event Loop Phase Order
1. TimerssetTimeout / setInterval callbacks past their threshold
2. Pending CallbacksDeferred I/O callbacks from the prior iteration
3. PollNew I/O events; can block here if idle
4. ChecksetImmediate() callbacks
5. Close Callbacks'close' event handlers (e.g. sockets)
Microtasksprocess.nextTick queue, then Promise queue — drain between every phase and callback
Stream Events & Methods
'data'Fired per chunk once in flowing mode
'end'No more data will be provided (Readable)
'drain'Safe to resume writing after write() returned false
'finish'All data has been flushed (Writable)
'error'Must be handled on every stream or pipeline() used
write(chunk)Returns false when internal buffer exceeds highWaterMark
pipeline(...)Preferred over .pipe() — forwards errors, auto cleanup
Cluster & Worker Threads APIs
cluster.fork()Spawns a worker process sharing the primary's listening socket
cluster.isPrimaryTrue in the primary process (isWorker in workers)
new Worker(file)Spawns a worker_thread running the given module
isMainThreadTrue only in the main thread, false inside a worker_thread
parentPort.postMessageSends a structured-cloned message to the main thread
SharedArrayBuffer + AtomicsZero-copy shared memory with safe concurrent access
Process, Env & CLI Flags
UV_THREADPOOL_SIZESets libuv threadpool size (default 4, must be set before startup)
--max-old-space-sizeCaps V8 old-generation heap size in MB
pm2 start app.js -i maxCluster mode, one worker per CPU core
pm2 reload appZero-downtime rolling restart of all workers
SIGTERMGraceful shutdown signal — drain, then exit
SIGKILLImmediate, non-catchable termination (last resort)
HTTP/API Essentials
429 Too Many RequestsStandard status for rate-limited clients
Idempotency-Key headerClient-supplied key to dedupe retried POSTs
4-arg middleware(err, req, res, next) — Express's error-handler signature
DataLoaderPer-request batching/caching to fix GraphQL N+1 queries
Cursor paginationStable pagination under concurrent inserts, unlike offset
Assessment Integration

Recommended Practice Quizzes on QuizCluster

Test your retention and prepare for timed live coding and MCQ technical screening rounds:

Frequently Asked Questions

Is Node.js really single-threaded, and does that matter for interviews?

Your JavaScript executes on a single thread, but libuv runs a background threadpool for blocking OS operations, and you can spin up worker_threads or cluster workers for true parallelism. Interviewers care most that you can precisely explain what runs where.

Should I prepare with Express or Fastify for backend interviews?

Express is still the most commonly assumed framework in interviews and take-home tests, so know its middleware model cold. Fastify knowledge is a strong bonus signal for performance-focused roles, but it's rarely a hard requirement.

How deep do I need to go on streams if I mostly build REST APIs?

Deep enough to explain backpressure and describe a Transform stream from memory — streams questions are a favorite way to test whether you understand memory and flow control, even if you rarely hand-write custom streams day to day.

If my team already deploys on Kubernetes, do I still need to know PM2?

Know the concepts PM2 encodes (cluster mode, zero-downtime reload, health-based restarts) even if Kubernetes handles orchestration in production — interviewers use PM2 as a concrete, well-known vocabulary for those same ideas.

Explore Other Preparation Guides

Software Engineering
How to Prepare for SDE Interview: Complete 2026 Roadmap
16 min readRead →
Java Ecosystem
How to Prepare for Java Developer Interview: Core to Spring Boot & JVM
18 min readRead →
Microservices & Distributed Systems
How to Prepare for Microservices Developer Interview: Distributed Architecture & Cloud
17 min readRead →
System Design
System Design Interview Guide: Complete 2026 Roadmap
21 min readRead →
Databases
SQL Interview Questions & Preparation Guide: Beginner to Advanced
17 min readRead →
Programming Languages
Python Interview Preparation: Complete Guide for 2026
17 min readRead →
Frontend Engineering
React Interview Preparation: React 19 & Next.js Guide
17 min readRead →
Cloud & DevOps
Kubernetes Interview Guide: Architecture, Pods, Networking & Troubleshooting
17 min readRead →
Cloud & DevOps
AWS Solutions Architect Interview Guide: Real Architecture Scenarios
17 min readRead →
Databases
Database System Design: SQL vs NoSQL, Sharding, Replication & Indexing
19 min readRead →
Microservices & Distributed Systems
Kafka Interview Guide: Architecture, Consumers, Partitions & Exactly-Once Semantics
17 min readRead →
Backend Engineering
REST API Design Interview Guide: Authentication, Pagination, Versioning & Rate Limiting
15 min readRead →
Cloud & DevOps
Docker Interview Guide: Images, Containers, Networking & Production Debugging
15 min readRead →
Programming Languages
JavaScript & TypeScript Interview Guide: From Closures to the Event Loop
17 min readRead →
Databases
Redis System Design Guide: Caching, Eviction, Persistence & Distributed Locks
17 min readRead →
Software Engineering
Concurrency Interview Guide: Threads, Locks, Race Conditions & Deadlocks
17 min readRead →
Software Engineering
Dynamic Programming Patterns: How to Recognize and Solve DP Problems
17 min readRead →