Node.js interview questions: Concurrency and scaling

Concurrency questions in Node.js are designed to expose category errors. Promise concurrency is not CPU parallelism, a worker thread is not a process, and adding replicas does not repair a shared downstream bottleneck. Interviews often begin with a slow task and ask whether you would use `Promise.all`, a worker pool, cluster, or another service. The senior signal is choosing from the work's actual constraints. The staff signal is defining queue bounds, cancellation, failure isolation, and the capacity model that keeps the fix from moving overload somewhere less visible.

The opening answers below reach for familiar primitives. The follow-ups make you price their consequences: serialization cost, memory duplication, event-loop contention, scheduling fairness, lost work, and coordination during deploys. Answer by naming whether the work is I/O-bound or CPU-bound, whether data must be shared, and what happens when arrivals exceed completions. The unlocked question demonstrates how to move from an API choice to a system design. Good practice here should leave you able to explain not just how work runs concurrently, but how the system behaves at its limit.

Covered in Volume 5: Workers, clusters, and scaling
Question 01Fully unlocked sample

Say you've got worker threads, child processes, and cluster all on the table. How do you figure out which one a given problem actually needs?

The strong answers pick the primitive by working out what needs shared memory and what needs to fail alone, instead of reciting the API list.

What an AI-prepared candidate might say

So they're for different things, basically. worker_threads gives you actual threads inside the same process, which is what you want for CPU-heavy stuff like parsing or image processing or crypto, and they can share memory through SharedArrayBuffer, plus they're cheaper than processes. child_process spawns a whole separate OS process. You'd use that to run other executables, or when you want real isolation, and it talks back to the parent over stdio or an IPC channel. And cluster forks a bunch of Node processes that all share one server port, so your HTTP server can use every core on the machine. The rule of thumb I know is cluster or your orchestrator for scaling requests, worker threads to get CPU work off the event loop, and child processes for external programs or anything you want isolated from the main app.

Senior

I ask three questions, in order. First, what's actually saturated? If the event loop is getting blocked by CPU-bound JavaScript, fine, parallelism will help. But if the service is I/O-bound, none of these primitives buys you any throughput, the loop already multiplexes I/O just fine and your fix is somewhere else entirely.

Second, what needs to be shared. Threads live in one process, so they can share ArrayBuffer memory zero-copy through SharedArrayBuffer, and they can transfer buffers between isolates without copying. Processes have to serialize everything over an IPC pipe, and that cost grows with the payload. So big binary inputs and outputs push me toward threads. If the tasks are small descriptors, processes are just as cheap, honestly.

Third, what needs to be isolated. A thrown error in a worker shows up as an 'error' event on the parent's handle and the process keeps going. But threads share the process. A native-addon crash or an OOM takes every thread down together. A child process is kernel-isolated, it can segfault or leak or get OOM-killed all on its own, and the parent just sees an exit.

One thing on weight, because people get this wrong constantly. Every worker thread is a full V8 isolate, its own heap, its own event loop. Cheaper and faster to spawn than a process, sure, but nothing like a goroutine. That's why pools exist. Cluster's the odd one out here, it's child_process.fork plus listening-socket coordination so N processes can serve one port, which gets you request-throughput scaling with process isolation and no shared memory. And if you need to run an external binary, there's exactly one door, and it's child_process.

Staff

At this level it stops being about the APIs and turns into sizing and deployment context. Threads and processes pull from the same core budget, and people forget that constantly. A cluster of eight workers where each one runs a four-thread pool, that's thirty-two runnable threads on an eight-core box. The oversubscription doesn't show up as throughput, it shows up as context-switch overhead and p99 jitter. I set the budget once, globally, from os.availableParallelism(), and every pool divides it.

Memory limits are per-isolate, which bites people. --max-old-space-size governs each process, worker threads take resourceLimits, and a fleet of isolates each entitled to a big heap can promise the container more memory than it actually has. And the OOM kill lands at the process level no matter which isolate did the allocating.

The Kubernetes question deserves a straight answer. If the platform's already running N single-process pods behind a service, cluster inside each pod is mostly duplicating the platform's job. The honest cases left are bin-packing big nodes with fewer, larger pods, per-core licensing shapes, or squeezing shared-nothing throughput out of a fixed VM. Otherwise one process per pod keeps health checks, memory accounting, and restart semantics lined up with what the orchestrator thinks is happening.

What actually picks the primitive is arithmetic on the workload. Count the bytes moved per task, serialization cost favors threads as payloads grow. Weigh tasks per second against spawn cost, though pools amortize that either way. Ask how much shared fate you can stomach, native code and memory pressure push you toward processes. And be honest about operational appetite, every thread pool you add is queueing and saturation and observability you now own inside the process, where the platform can't see any of it.

Follow-up chain

  1. People say worker threads share memory. What actually gets shared?
  2. Okay, so if I postMessage a 100 MB object across, what does that cost me, and how do I avoid paying it?
  3. Say a native addon segfaults inside one of your worker threads. How much goes down with it?
  4. You're already on Kubernetes. Is cluster ever still worth running?
Question 02First answer included

So when does moving work onto a worker thread actually make things faster, and when does it quietly make them slower?

Good candidates price the offload first, serialization, scheduling, the clone back, and they know SharedArrayBuffer and Atomics are the escape hatch.

What an AI-prepared candidate might say

They help when the work is CPU-bound. So heavy parsing, compression, image manipulation, cryptography, that kind of thing, because those block the event loop and a worker gets them off the main thread. They don't help with I/O-bound work, Node already handles that asynchronously. And they can actually hurt when the tasks are small, because spawning threads and passing messages has overhead, which is why everyone uses a pool that reuses workers. The rule of thumb I've seen is that the computation should be big enough to outweigh the cost of sending the data over and getting the result back. Then SharedArrayBuffer lets threads share memory instead of copying it, and Atomics gives you safe reads and writes and waiting on that shared memory. That's how you avoid races when multiple threads are touching the same buffer.

Senior

The actual offload arithmetic, compute against the clone both ways, why the JSON.parse example usually loses, and the thing Atomics.wait does on Node's main thread.

Staff

The before-and-after numbers I make teams collect, the GC signature clone-heavy designs leave behind, and when the honest answer is leaving Node entirely.

Follow-up chain

  1. So you push JSON.parse into a worker and the win just disappears. Where did it go?
  2. What shapes of data actually dodge that clone cost?
  3. What happens if you call Atomics.wait on Node's main thread?
  4. Say two workers share a SharedArrayBuffer and you've got no lock library. How do they coordinate?
Question 03First answer included

Node's cluster module, when a connection comes in, how does it actually decide which worker gets it?

A strong answer follows a connection from accept to worker under both scheduling modes, and can say why long-lived connections defeat round-robin.

What an AI-prepared candidate might say

Cluster forks a bunch of Node processes that cooperate so one port serves all of them. With the default scheduling policy, which is round-robin, the primary process accepts the incoming connections and hands them out to workers in turn, so the load stays roughly even. On Windows the default is different, the OS decides which process gets each connection there. Every worker runs your full server code. For long-lived stateful protocols like WebSockets, or Socket.IO holding session state, you need sticky sessions, meaning the same client always lands on the same worker, and round-robin doesn't guarantee that, so you add affinity at a proxy or hash by client address or something like that. There's also SO_REUSEPORT as an alternative to the whole arrangement, where each worker opens its own listening socket and the kernel spreads the connections between them.

Senior

Which process really owns the listening socket under SCHED_RR versus SCHED_NONE, how a live TCP connection crosses a process boundary, and why sticky sessions exist.

Staff

Diagnosing a hot worker when connection counts look even, the keep-alive fairness problem, and an honest look at letting the kernel distribute via SO_REUSEPORT.

Follow-up chain

  1. The connections are spread evenly, but one worker is pinned at 100 percent CPU. How does that happen?
  2. Alright, what do you actually change to fix it?
  3. Mechanically, how does the primary hand a live TCP connection over to another process?
  4. And if you switch on SO_REUSEPORT, how much of this picture changes?
Question 04First answer included

When you send data between processes or worker threads in Node, what does that actually cost you?

Strong candidates know every message means serialize, copy, deserialize plus backpressure, and design protocols around ids and transfers, never big payloads.

What an AI-prepared candidate might say

So nothing travels by reference between processes or workers, it all gets copied. process.send to a child process serializes the message, JSON-style by default, or there's an advanced mode based on structured clone, and writes it over the IPC pipe. postMessage between worker threads structured-clones the value into the receiving thread. Plain data is fine either way, and structured clone also handles Buffers, Maps, Sets, circular references, that kind of thing. The cost grows with the message size, so big payloads get slow and memory-hungry. There are two escape hatches I know of. Transferables move ownership without copying, so an ArrayBuffer or a MessagePort listed in the transfer list detaches on the sender's side and reappears on the receiver's. And SharedArrayBuffer goes further, both sides read and write the same memory and nothing gets copied at all.

Senior

What each channel's pipeline actually does, JSON versus structured clone versus advanced serialization, where the copies land, and what a transfer list really moves.

Staff

Spotting serialization in a CPU profile, the protocol for when send starts returning false, and the redesigns that shrink messages by orders of magnitude.

Follow-up chain

  1. You're under load and process.send starts coming back false. What's happening, and what do you do?
  2. And if you just ignore it, what breaks?
  3. What exactly changes when you list an ArrayBuffer in the transfer list?
  4. Two workers need to trade data directly, no relaying through the main thread. How do you wire that?
Question 05First answer included

Say your Node service has maxed out the box it runs on. What has to be true before you can just throw more machines at it?

The good answers name every place per-process state hides, then do the shared-resource math before anyone gets to add instances.

What an AI-prepared candidate might say

It basically has to be stateless. Anything that needs to survive past a request goes to shared infrastructure, so sessions go in Redis or a signed cookie, uploads go to object storage, background jobs go on a queue, and that way any instance can serve any request. Then a load balancer spreads traffic across the instances, with health checks to pull bad ones out of rotation, and sticky sessions only if something really needs affinity. Shared state lives in databases and caches that every instance can see. Once that's all true, scaling is just adding instances behind the balancer, and an autoscaler can do it off CPU or request metrics. The usual blockers are in-memory sessions, writing to local disk, or code that just assumes there's one instance. You fix all of those by moving the state out of the process.

Senior

The full list of places state hides (sessions, caches, limiters, singleton jobs, sockets, local disk) and what each one does to you at N instances.

Staff

The DB-connection budget, cache stampedes at fleet scale, the metric a Node service should actually autoscale on, and keeping rollouts safe under version skew.

Follow-up chain

  1. Say every pod runs a DB pool of 20 and you scale out to 50 pods. What gives out first?
  2. Okay, what are the realistic options at that point?
  3. Where do rate limiters that live inside each process go wrong, and what do you replace them with?
  4. One instance used to run the nightly cleanup job, and now there are ten of them. How do people handle that?
Question 06First answer included

Let's design a worker pool for CPU-bound jobs. What decisions do you actually have to make?

Strong candidates make saturation behavior an explicit decision someone signed off on, and watch queue wait time before any other health signal.

What an AI-prepared candidate might say

You size the pool around the number of CPU cores, and you reuse workers instead of spawning one per task, since that amortizes the startup cost. Then you put a queue in front so tasks can wait when every worker is busy. Each task goes out to a free worker with postMessage and the result comes back the same way. When it saturates you either queue with a limit, reject new tasks, or push backpressure up to the caller. You want per-task timeouts too, so a stuck task doesn't hold a worker forever, and you restart workers that crash. Honestly, libraries like piscina package most of this up already, the sizing, the queueing, the timeouts, so hand-rolling it is rarely necessary. And you watch queue depth and task latency to tell when the pool is undersized.

Senior

Sizing from availableParallelism minus a core for the main thread, why the queue is always bounded, per-task deadlines, and the terminate-and-replace lifecycle.

Staff

Queue wait time as the real health signal, the task-skew and poison-task pathologies, when to recycle workers, and bulkheading pools per workload class.

Follow-up chain

  1. Queue wait time keeps climbing but the workers are sitting at 60 percent CPU. Where do you look first?
  2. And when it turns out to be task-size skew, how do you fix that?
  3. Why does a hung task have to be terminated? Why can't you just send it a cancel message?
  4. Walk me through how an unbounded task queue actually fails.
Question 07First answer included

Walk me through deploying a new version of a Node service without dropping a single request.

A good answer ties each deploy-window error signature to its specific race and owns the app's side of the contract a rolling restart depends on.

What an AI-prepared candidate might say

You run multiple instances and replace them one at a time. Bring up a new instance, wait for its health check to pass, shift traffic over to it, then send SIGTERM to an old one, and that kicks off graceful shutdown, so it stops accepting new connections, finishes the in-flight requests, and exits before the platform's grace period runs out. Kubernetes rolling updates, blue-green, canary, they're all basically the same swap with a different order and blast radius. The app really has two jobs in all of this. Handle SIGTERM properly, and report readiness accurately so the balancer only routes to instances that can actually serve. If you do it right, capacity stays above demand the whole time and clients never notice anything. The usual failures are instances getting killed mid-request, or traffic getting sent to instances that aren't ready yet.

Senior

The app's side of a rolling deploy (readiness choreography, keep-alive teardown toward the LB, warmup before ready) and which race fires when a piece goes missing.

Staff

Deploy-window error rate as its own SLI, version-skew discipline and expand-migrate-contract, the capacity math of a rollout, and the WebSocket migration design.

Follow-up chain

  1. Every deploy shows a two-second burst of connection-refused errors. Where's the race?
  2. So what's the standard ordering that mitigates it?
  3. What actually breaks for WebSockets during rolling restarts, and what do you do about it?
  4. Why are the new pod's first hundred requests slow, and is that even worth fixing?
Question 08First answer included

In-process cache versus something shared like Redis, how do you actually make that call when you're running a fleet?

Strong answers weigh hit-rate dilution, invalidation reach, and GC cost before placing a cache, then defend a layered design with staleness bounds.

What an AI-prepared candidate might say

An in-process cache, so a Map or an LRU living inside the instance, is the fastest thing you can do. No network hop, no serialization, it's just a heap read. The downsides are duplication, since every instance caches its own copy, inconsistency between instances, and you lose it all on every restart. A shared cache like Redis gives all the instances one view, survives deploys, and each key gets cached once. But you pay a network round trip and serialization on every hit, plus it's a new dependency you have to operate. The pattern I've seen is layering them, a small short-TTL in-process cache in front of Redis in front of the origin, so hot keys are nearly free and the shared layer keeps the fleet roughly consistent. Which way you lean depends on how hot the data is, how much staleness you can tolerate, and how expensive the origin fetches are.

Senior

The mechanics that actually move the decision, tail keys going cold at N instances, staleness windows times fleet size, heap caches as GC ballast, and heap hit versus RTT.

Staff

The layered L1/L2 design with a staleness contract per layer, stampede controls that hold across processes, and the Redis-outage fallback you settle before the incident.

Follow-up chain

  1. You go from 5 pods to 25 and the aggregate hit rate drops. What's the mechanism there?
  2. A hot key expires and the whole fleet piles onto the origin at once. What are the standard fixes?
  3. How do you make single-flight work across processes, not just inside one?
  4. What does a large in-process cache actually do to your GC?
Question 09First answer included

Node gives you spawn, exec, execFile, and fork. What's actually different between them?

Good candidates know all four APIs wrap spawn, and can reason from stdio mechanics to the full-pipe deadlock and the kill that leaves orphans behind.

What an AI-prepared candidate might say

They all run another program, the differences are mostly ergonomics. spawn is the base one. It starts the process and gives you streams for stdin, stdout, and stderr, so it fits long-running processes or big output. exec runs your command through a shell and buffers the entire output, then hands it to a callback. That works for short commands with small output, because I'm pretty sure the buffer has a size cap on it. execFile is basically exec without the shell, it runs the binary directly with an argument array, so it's faster and safer with untrusted input. And fork is spawn specialized for Node scripts. It launches a new Node process and sets up an IPC channel, so parent and child can exchange messages with send and the 'message' event. That's the primitive sitting under cluster and process pools.

Senior

One primitive, three wrappers. Where the shell sneaks in, what maxBuffer really caps, how fork wires up IPC, and how the stdio modes decide who feels backpressure.

Staff

The stalled-child postmortem, killing whole process trees without leaving orphans, and the review habits that keep exec injection out of a codebase.

Follow-up chain

  1. Your spawned child hangs forever, but the exact same command finishes instantly in a terminal. What's your first hypothesis?
  2. And why does switching to stdio: 'inherit' make the hang disappear?
  3. You killed the child, but its grandchildren are still running. Why, and how do you fix it?
  4. Where does exec's maxBuffer actually bite people, and what do you use instead there?
Question 10First answer included

You've got long-running workers that crash sometimes. How do you supervise them so a crash doesn't take the service down, but you also don't end up restart-looping forever?

A strong answer builds supervision in layers, heartbeats beyond exit events, backoff under a budget, and a plan for the task the dead worker was holding.

What an AI-prepared candidate might say

You listen for the worker's exit and error events, log whatever happened, and start a replacement. To avoid a tight crash loop you add exponential backoff between restarts and cap the number of attempts, and past the cap you alert instead of respawning. You read the exit code to tell a clean exit from a crash. And the restart logic stays outside the worker itself, so a broken worker can't break its own recovery. Health checks or heartbeat messages catch the workers that hang without actually dying. Then at the outer layer, a process manager like systemd or PM2 or the orchestrator supervises the parent the same way. The goal is basically automatic recovery from transient failures, containing the repeated ones, and enough logging to figure out what killed the worker.

Senior

Reading exit codes and signals properly, heartbeats for the worker that's wedged but alive, backoff with a restart budget, and deciding what happens to in-flight tasks.

Staff

Supervision layered with escalation, MTBF trends as your leak detector, poison-task quarantine, and the crash forensics (reports, dmesg, signals) that make restarts explainable.

Follow-up chain

  1. A worker just exited with code null and signal SIGKILL. What happened to it?
  2. How would you pin that on the OOM killer specifically?
  3. One job kills every single worker that picks it up. What's the pattern for that?
  4. If you're already handling exit events, why bother with heartbeats?

THE BASELINE GETS YOU THROUGH QUESTION ONE.

Raw Mode trains the follow-ups.

Unlock every Senior and Staff answer, every tree answer, the debug repos, hallucination drills, design scenarios, and the framework capstone.Get NodeBook Raw Mode