Your Node service has low CPU but latency spikes for every request at the same moment. What is happening?
Something synchronous is occupying the single thread that runs your JavaScript, so every pending request waits behind it regardless of what those requests are doing. The signature is that latency rises for everything at once rather than for one endpoint, which is what distinguishes it from a slow dependency.
What the interviewer is scoring
- Does the candidate recognise that the symptom affecting all requests points at the shared thread
- Whether they can name realistic sources of blocking beyond an obvious busy loop
- That async syntax is understood not to make CPU work concurrent
- Whether they know how to measure event loop delay rather than guessing
- Does the candidate choose between worker threads, chunking and a separate service on the right grounds
Answer
Read the symptom
The detail that identifies this is that everything slows down together. A slow database query makes the endpoints that use it slow. A blocked event loop makes the health check slow, at the same instant, even though the health check touches nothing.
That pattern points at a shared resource, and in Node the shared resource is the single thread that executes your JavaScript. While a synchronous function is running on it, nothing else progresses: not the callback for a completed database query, not the parsing of an incoming request, not a timer. The work is finished and the results are waiting; there is no thread available to deliver them.
The low CPU reading is consistent with this and often misleads people. One saturated core on an eight-core machine is 12% utilisation, which looks idle on a dashboard while the process is entirely stalled.
Where blocking comes from
It is rarely a while loop someone left in. The realistic sources are ordinary operations at unexpected sizes.
JSON.parse and JSON.stringify are synchronous and scale with document size. A megabyte payload is tens of milliseconds of pure blocking, and it is often the response serialisation rather than the request parsing.
Cryptography: crypto.pbkdf2Sync, bcrypt used synchronously, or signing in a loop. Password hashing is deliberately slow, so a synchronous hash blocks for exactly as long as it was tuned to take.
Regular expressions with catastrophic backtracking, where nested quantifiers turn a moderately long input into exponential work. This is the source that most often arrives as an attack, because the input is usually user-supplied.
Large array operations — sorting, mapping, or a find inside a loop over a hundred thousand records — and any of the *Sync filesystem calls, which are entirely reasonable at startup and entirely wrong inside a request handler.
What async does not do
The most common misconception, and worth being explicit about. Marking a function async does not move its body off the thread.
// Still blocks for the entire hash. `async` changes how the result is
// delivered; it does not change where the work runs.
async function hash(password) {
return crypto.pbkdf2Sync(password, salt, 600_000, 32, "sha256");
}
async and await are about scheduling — when a continuation runs relative to other work — not about parallelism. Asynchronous I/O is non-blocking because the operating system performs it elsewhere and notifies the loop when it is done. CPU work in JavaScript has no such elsewhere. There is one thread, and computation occupies it until it finishes.
The corollary is that await on something that is not genuinely asynchronous buys nothing at all, and awaiting a resolved promise merely defers the rest of the function to the microtask queue, which does not let any other request through.
Measure it
The metric is event loop lag: schedule a timer for a known interval and measure how much later it actually fires. If a 50 ms timer fires at 500 ms, the loop was occupied for 450 ms.
perf_hooks.monitorEventLoopDelay gives you this as a histogram with percentiles, and it belongs in production telemetry permanently rather than being added during an incident. It is the single most diagnostic number a Node service produces: p99 loop delay tracks user-visible latency more closely than CPU or memory ever will.
To find the culprit rather than confirm the diagnosis, take a CPU profile while it is happening. The blocking function sits at the top of the flame graph as a wide synchronous block, which is unusually easy to read.
Fixing it, in order of cost
Do less. Often the whole answer. Do not serialise a 5 MB response — paginate. Do not sort a hundred thousand records in the handler — make the database do it, where it is a different process on a different core. Cache the expensive derivation. This is worth exhausting first because every other option adds architecture.
Use the async variant. Node's crypto functions have callback forms that run on the libuv thread pool, which is genuinely off the main thread. Same for filesystem operations. Frequently a one-word change.
Chunk the work. If it must happen in-process and is naturally divisible, process a slice, yield with setImmediate, and continue. Total time increases slightly; the loop gets to serve other requests between slices. Suits batch work that happens to run inside a request-handling process.
Worker threads. For sustained CPU work — image processing, large compression, a genuine computation. Real parallelism, at the cost of message-passing and no shared memory beyond SharedArrayBuffer. Note the overhead: spawning a worker costs milliseconds, so use a pool, and do not use one for work measured in microseconds where the messaging costs more than the task.
A separate service. When the CPU work is the product rather than an incidental part of it, at which point it should scale and deploy on its own terms.
Every request slowing at once is the signature of a shared thread, not a slow dependency. Watch event loop delay and you will see it before your users do.
Likely follow-ups
- Does making the function `async` help? Why not?
- How would you measure event loop lag in production?
- When is a worker thread the right answer, and when is it overhead?
- Which built-in operations block that people assume do not?
Related questions
- Walk me through the phases of the Node event loop.mediumAlso on event-loop and performance4 min
- A read-only endpoint that returns fifty thousand rows is slow and memory-heavy in EF Core. What is the context doing?mediumAlso on performance4 min
- A request fails but your Express error-handling middleware never runs. How do you work out why?mediumAlso on nodejs3 min
- Walk me through how you read a PostgreSQL execution plan.mediumAlso on performance5 min
- Field data says your page has an INP of 400 ms. How do you find the cause and fix it?hardAlso on event-loop4 min
- A query has an index on the filtered column but the plan shows a sequential scan. Walk me through how you would diagnose it.hardAlso on performance3 min
- How does the .NET garbage collector decide what to collect, and how would you make a hot path allocate less?hardAlso on performance6 min
- How does Node load your modules, and how would you make a Node service use more than one core?mediumAlso on worker-threads6 min