Node.js interview questions: Performance and observability

Performance interviews are tests of experimental discipline. The prompt may claim that Node.js is slow, memory is high, or one endpoint regressed after a deploy. Weak answers immediately prescribe caching, workers, or a larger machine. Strong answers define the symptom, choose a measurement that can separate competing explanations, and preserve enough context to connect a runtime signal to user-visible latency. Staff-level reasoning also accounts for coordinated omission, cardinality, sampling, and the cost of the instrumentation itself.

This question set is built around evidence chains. Opening answers mention profilers and metrics; follow-ups ask which metric, at what aggregation, under what workload, and what result would change the proposed fix. Practice moving from RED-style service signals to event-loop delay, CPU profiles, allocation data, and trace boundaries without collecting everything by default. The unlocked sample shows how capacity math and targeted instrumentation make an answer credible. You should finish each chain with a reproducible experiment, a bounded change, and a before-and-after number rather than a performance story that cannot be falsified.

Covered in Volume 5: Profiling and observability
Question 01Fully unlocked sample

Say you've got a Node service that's slow and CPU is pegged. Walk me through how you'd actually find the bottleneck instead of guessing at it.

A strong answer reaches for a CPU profile and a flame graph to see where the time really goes, instead of guessing and optimizing on gut feel

What an AI-prepared candidate might say

So the first thing is you measure, you don't guess. I'd grab a CPU profile. Node has a built-in profiler you run with --prof, or you can attach Chrome DevTools through the inspector, and there's also tools like clinic or 0x that give you a flame graph. Basically the flame graph shows which functions are eating the CPU, the widest bars are where the time is going, so you look at those and optimize that hot path. I'd also want to check whether it's really CPU or actually I/O, because if the process is just waiting on the database or the network then making the CPU code faster won't help. You'd look at response time, throughput, event-loop lag, that kind of thing, to tell them apart. And after a change you profile again to confirm it actually got better. Let the data drive it instead of intuition, basically.

Senior

First thing I want to know is whether we're actually burning CPU or just waiting on something, because those need different tools and picking wrong wastes the investigation. A CPU profile samples the call stack at some fixed interval, so it tells you where the process is spending cycles. Great when you're CPU-bound, computation, serialization, parsing. But it's blind to waiting. A stack that's parked in a socket read isn't consuming CPU, so it barely shows up at all.

The flame graph is just a rendering of that profile, and the thing people get wrong is the horizontal axis. It's not time-ordered. A frame's width is the proportion of samples where that function was on the stack, so width is CPU cost, period. Stacking is call depth, each frame sits on whatever called it. So you scan for wide frames near the top. A wide top frame is code that's actually running on-CPU. A wide frame down low with a bunch of narrow children is just a caller whose cost lives in its descendants. And if you see a plateau, one function wide across the top, that's your hot spot.

That model gives you predictions to check. High CPU plus one obvious wide tower, go after the tower. High CPU but a flat graph with no dominant frame, the cost is spread out, or the profiler is catching garbage collection and framework overhead, which points at allocation pressure rather than one slow function. And if the service is slow but CPU isn't actually high, the profile is the wrong instrument entirely. The time is in I/O wait or event-loop scheduling, so you go measure event-loop delay and I/O timing instead. Knowing what the profile can and can't see is what stops you optimizing code that was never the bottleneck.

Staff

In production I profile the live process, I don't take it down. Partly because taking a service down to diagnose it is usually not an option, and partly because a profile from a synthetic environment misses the real hot path more often than you'd think. You can enable the inspector on a running process and pull a CPU profile over the inspector protocol, or use node --prof, or build the capture into the app with a programmatic Session. I grab a bounded window on one instance under real load, then ship the profile off to analyze it, so the overhead stays small and time-limited.

The trap I've actually been burned by is treating the CPU profile as the whole story. A lot of these slow-and-high-CPU incidents are really event-loop starvation. Some synchronous stretch blocks the loop, requests pile up behind it, and the symptom is high CPU with rising latency, and the naive read blames whatever frame happens to be widest. So I always pair the profile with event-loop delay from monitorEventLoopDelay. If the lag spikes line up with the latency spikes, the fix is getting that synchronous work off the loop. You could shave microseconds off the hot function all day and nothing would move.

And I won't ship a fix on the strength of a flame graph alone. I take a baseline first, p99 latency and CPU per request under a fixed load, make the change, then re-run the identical load. If p99 and CPU-per-request don't move, the flame graph misled me about what actually mattered, and I revert instead of keeping a complexity-adding change that bought nothing. The whole loop stays measured. Identify with a profile, confirm the class of bottleneck with event-loop delay, fix it, prove it with the controlled before-and-after.

Follow-up chain

  1. Okay, you've got a CPU profile open. When you look at the flame graph, what's the width of a frame actually telling you, and what about the stacking?
  2. Say the flame graph shows your handler is cheap, but requests are still slow. Where's that time hiding?
  3. How would you pull a CPU profile off a live production process without taking it down?
  4. So you found the hot function and optimized it. How do you prove that actually helped?
Question 02First answer included

If you could only watch one metric to catch a Node process going bad before users notice, what would it be? And how do you measure it properly?

A strong answer picks event-loop delay as the metric that moves first when a Node process goes bad, and measures it as a histogram instead of an average

What an AI-prepared candidate might say

I'd say event-loop lag, or event-loop delay, same thing. Node runs your JavaScript on a single thread, so if anything blocks the event loop, every request gets delayed, and the thing is CPU and memory can still look totally fine while that's happening. So watching the lag tells you when the loop is falling behind. You measure it with perf_hooks, there's a monitorEventLoopDelay function that records the delay into a histogram. If the lag is rising it means the loop can't get to events promptly, usually because of synchronous or CPU-heavy work somewhere. You'd set an alert threshold, maybe tens of milliseconds or something like that, and investigate when it crosses. The usual fix is moving the blocking work off the main thread, into a worker thread or an async operation. It catches problems the CPU and memory graphs just miss, so it's the one to watch first.

Senior

Why event-loop delay beats CPU at predicting the latency users feel, how `monitorEventLoopDelay` builds its histogram, and what it means when p99 lag starts climbing.

Staff

How to pick a threshold that catches real stalls without crying wolf, tying lag spikes back to the synchronous code behind them, and why 'add more CPU' almost never fixes it.

Follow-up chain

  1. How is it that CPU can look totally healthy while the event loop is stalled and requests are piling up?
  2. Okay, so what kind of code actually causes a lag spike like that, and how do you track down which handler it was?
  3. Why bother with a histogram for event-loop delay? What's wrong with just reporting the average?
  4. Where would you actually set the alert threshold for lag? And what's the danger in picking one fixed number?
Question 03First answer included

Say memory on one of your Node services just keeps climbing in production. How would you dig into that without bouncing the process?

A strong answer keeps RSS, heapUsed, and external memory straight, and can chase a production leak without ever bouncing the process

What an AI-prepared candidate might say

I'd start with process.memoryUsage() and look at which number is actually growing. RSS is the total resident memory the process is holding. heapUsed is how much of the V8 heap is in use, and external is memory from C++ objects bound to JavaScript, Buffers mostly. If heapUsed keeps growing it's probably a JavaScript leak, objects being retained that should have been collected. So I'd take heap snapshots at intervals and compare them in Chrome DevTools to see what's piling up. Usual suspects are unbounded caches, event listeners nobody removed, closures holding references, that kind of thing. If it's external that's growing, that points at Buffers or native memory instead. Basically look at what allocates over time and whether the references ever get released, then fix whatever code is holding them. Restarting just masks it, so the goal is finding the actual retention.

Senior

What RSS, heapUsed, and external each actually measure, why a Buffer leak will never show up in heapUsed, and how you tell a growing retained set from normal churn.

Staff

How to capture comparable heap snapshots off a live process, read the diff for the retaining path, and tell a real leak apart from GC that just hasn't run yet.

Follow-up chain

  1. Say heapUsed is flat but RSS just keeps growing. What's leaking there, and where do you go looking?
  2. How would you confirm that's Buffers or native memory and not just heap fragmentation?
  3. How do you actually take a heap snapshot on a production process? And what's the cost you have to plan around?
  4. Memory climbed for an hour and then dropped off sharply. Was that a leak? How would you tell?
Question 04First answer included

Suppose your p99 shows these periodic spikes and they line up with garbage collection. How do you think about that, and what would you actually change?

A strong answer ties GC pauses to the latency-tail spikes and reaches for allocation rate as the first lever, long before any GC tuning flag

What an AI-prepared candidate might say

So V8's garbage collector is generational. Most objects die young and get collected quickly in the young generation by minor GCs, which are fast. Objects that survive get promoted to the old generation, and that gets collected by major GCs, which are more expensive and can pause the process. And since Node runs your JavaScript on one thread and GC runs on that same thread, a long major GC blocks your request callbacks, which is basically the latency spike you're seeing. To cut the impact you want to make less garbage. Avoid allocations you don't need, reuse buffers and objects, drop references you're done with. There are flags like --max-old-space-size to tune the heap, but that's kind of a blunt tool. Lowering allocation pressure is usually the better move, since GC then runs less often and has less to do. --trace-gc or performance hooks tell you how much time actually goes to collection.

Senior

Why the generational design keeps minor GC cheap while major GC is the pause that hurts, how that pause turns into tail latency, and why allocation rate drives all of it.

Staff

How to measure GC pause time against your SLO, cut allocation pressure before you touch a single flag, and tell when a heap-size change helps and when it just hides the problem.

Follow-up chain

  1. So why is it the major GCs that hurt your latency tail, when the minor ones mostly don't?
  2. And what is it in your code that decides how often a major GC has to run?
  3. Say a teammate wants to bump --max-old-space-size to fix the spikes. When does that actually help, and when are you just trading up to a bigger pause later?
  4. How would you actually put a number on GC's share of your latency instead of guessing at it?
Question 05First answer included

You'll notice experienced engineers basically refuse to look at average latency. Why is that, and what do they look at instead?

A strong answer thinks in percentiles and histograms instead of averages, and can explain why the tail is what users actually feel

What an AI-prepared candidate might say

Averages hide the tail, basically. If most requests are fast but some small fraction are really slow, the average still looks fine while those users are having a terrible time. p99 is the value that 99% of requests come in under, so it captures that slow tail, the worst case real users actually hit. That's why people watch percentiles like p95, p99, p99.9 instead of the mean. And to compute percentiles correctly you need the distribution of latencies, which is what histograms give you. They bucket the response times so you can calculate percentiles and aggregate across servers. A single average per server can't really be combined in any meaningful way. The tail matters too because in systems making lots of calls, the slow ones end up dominating the overall response time. So you track latency as a histogram and watch the high percentiles, and those tell you what your slowest users are experiencing.

Senior

What an average quietly erases, why a high-fan-out request lives in the tail, and why you can't just average percentiles across your hosts.

Staff

How histograms let you aggregate latency correctly, why one slow dependency ends up dominating a fanned-out request, and which percentile you actually hold yourself to.

Follow-up chain

  1. Say a request fans out to 20 backends and has to wait for all of them. Why does one backend's p99 end up being the typical experience for the whole request?
  2. Okay, so how do you cut down that tail amplification without going and making every backend faster?
  3. Why can't you just take the p99 from each of your ten instances and average them?
  4. So which percentile do you actually put the SLO on, and how do you defend that choice?
Question 06First answer included

When you load test a Node service, how do you make sure the numbers aren't lying to you?

A strong answer knows the ways a load test lies, skipped warmup, coordinated omission, closed-model backpressure, and designs the test around all three

What an AI-prepared candidate might say

There's a bunch of ways a load test can trick you. First, warm up before you measure, because the first requests hit cold caches and code that hasn't been optimized yet, so the early numbers don't represent steady state. You want realistic traffic patterns and payloads too, not some trivial endpoint. Then there's coordinated omission, where the tool waits for a slow response before sending the next request. That hides the true tail latency since the delayed requests never get counted, so you use tools that account for it. There's also open versus closed models, open means requests arrive at a fixed rate regardless of responses, closed means a fixed number of clients each wait for a response. Open is closer to real traffic, I think. Report percentiles since averages hide the tail, find the point where the service degrades, and test somewhere close to production so the results transfer.

Senior

Why warmup, connection reuse, and your choice of load model all bend the numbers, and what coordinated omission quietly does to the tail latency you report.

Staff

How to build a test that actually reproduces production, open model, realistic connection reuse, corrected latency, then find the knee where the service really falls over.

Follow-up chain

  1. So what exactly is coordinated omission, and which way does it skew your latency numbers?
  2. And how do you run a load test that doesn't fall into that trap?
  3. Walk me through open model versus closed model. How does that choice change what an overload test even measures?
  4. Why is skipping warmup such a problem for a Node service in particular? What does it do to the test?
Question 07First answer included

Let's say you're adding a cache in front of a hot read path. What are the decisions that actually determine whether it helps or blows up on you?

A strong answer picks the cache layer by its consistency and invalidation behavior, and knows a stampede can make a cache hurt more than it helps

What an AI-prepared candidate might say

A cache makes sense when reads happen a lot, the data's expensive to fetch, and it doesn't change too often. The main decision is where to put it. An in-process cache is fastest because it's just memory in the same process, but each instance keeps its own copy, so instances can disagree. Something shared like Redis stays consistent across instances and holds more, but you pay a network hop. You set a TTL so entries expire, and you still need an invalidation plan for when data changes. The known problem is a cache stampede, where a popular item expires and a bunch of requests all recompute it at once and overload the backend. You prevent that with a lock so only one request recomputes, or by refreshing before expiry. And you watch the hit rate to confirm it's doing its job. Really it depends on your consistency and performance needs.

Senior

The three axes that actually matter, placement, consistency, and invalidation, plus why an in-process cache drifts per instance and how a hot key's expiry turns into a stampede.

Staff

How you stop stampedes with a lock or early recompute, pick a placement that fits your consistency needs, and why hit rate and origin load only make sense measured together.

Follow-up chain

  1. So an in-process cache and a shared Redis behave differently on consistency. What actually breaks with the in-process one once you're running a fleet?
  2. Given all that, when would you still pick a per-instance in-process cache?
  3. Say a really popular key expires and every instance goes to recompute it at the same time. What's that called, and how do you stop it?
  4. Everyone says invalidation is the hard part. What makes it harder than just setting a TTL and moving on?
Question 08First answer included

In Node, how does distributed tracing actually follow a request across all those async boundaries? And what does that machinery cost you at runtime?

A strong answer explains how `AsyncLocalStorage` carries context across async boundaries and what that machinery actually costs at runtime

What an AI-prepared candidate might say

So tracing works by giving each request a trace ID and carrying that ID through all the work done for it, including across service calls in headers. The tricky part inside a Node process is that the work happens across async callbacks, so a normal local variable doesn't survive. Node has AsyncLocalStorage, which stores data that stays associated with the current asynchronous execution context, so any code running as part of that request can read the trace context without passing it around explicitly. Under the hood it uses async_hooks to track async operations, I believe, and that tracking adds overhead to every async operation, so tracing isn't free. Turning it on can measurably slow the app down. That's why you sample, only tracing a fraction of requests. And between services the trace context travels in HTTP headers, there's a standard, W3C Trace Context, so the trace continues in the next service.

Senior

How `AsyncLocalStorage` gets context across awaits with zero manual threading, the `async_hooks` machinery underneath, and where that per-operation cost really comes from.

Staff

How to decide what's worth tracing and at what sampling rate once you know the runtime cost, carry context across service boundaries, and measure the overhead you actually added.

Follow-up chain

  1. So without threading a trace ID through every function signature, how does AsyncLocalStorage get the context to code way down in an async call chain?
  2. And where does the runtime overhead of that mechanism actually come from?
  3. Now the trace has to cross a network call into another service. What actually travels across, and how?
  4. You can't afford to trace everything. So how do you sample without throwing away the traces you actually need?
Question 09First answer included

Logging feels like the most boring part of the stack, right up until it's your bottleneck. What makes logging hard at scale in Node, and why is pino designed the way it is?

A strong answer treats logging as a performance surface, structured output, async transport, sampling, and can say why pino is fast where a naive logger isn't

What an AI-prepared candidate might say

At scale logging becomes a performance and cost thing. Structured logging means emitting logs as JSON with fields instead of plain strings, which matters because you can query and filter by field in your log aggregation system, and you need that once the volume gets big. Plain string logs are hard to search and parse. Logging itself isn't free either, serializing objects and writing them costs CPU, and if the writes are synchronous or the destination is slow, it can block the event loop. Pino is the popular fast logger for Node. It does minimal work on the main thread, produces the JSON efficiently, and can push the writing and processing off to a separate transport or worker so it doesn't slow down request handling. At high volume you also sample, keep a fraction of the noisy high-frequency logs to control cost but keep the errors. So you get visibility without logging becoming the bottleneck.

Senior

Why unstructured logs stop being queryable at volume, what synchronous logging does to your event loop, and the design choices that keep pino cheap on the hot path.

Staff

How to keep logging off the hot path, sample high-volume logs without losing the ones you'll need, and the ugly failure where a slow log destination stalls the whole process.

Follow-up chain

  1. Once you've got real volume, why is a structured JSON log line worth so much more than a nicely formatted human string?
  2. And what does pino actually do to keep the cost of producing that line off the request path?
  3. console.log can write synchronously in some cases, right? How does that end up stalling your event loop?
  4. Say you're logging way too much to store it all. How do you sample without throwing away the logs you actually need?
Question 10First answer included

How would you figure out how many instances a Node service actually needs? And when do you add instances versus just making the code faster?

A strong answer builds capacity from measured per-core throughput and makes the scale-out versus optimize call from where the bottleneck actually sits

What an AI-prepared candidate might say

Start by measuring what one instance can handle. A Node process runs your JavaScript on one thread, so one process uses roughly one CPU core for request work, and on a multi-core machine you run several processes with the cluster module, or several containers, to cover all the cores. Then you load test one instance to find how many requests per second it sustains inside your latency target. Divide expected peak traffic by that number, add some safety margin, and that's your instance count. You monitor CPU, event-loop lag, and latency so you know when you're near capacity. Whether to scale out or optimize depends on the bottleneck, I'd say. If CPU is spread across lots of requests, more instances help. If it's a single slow function or a blocked event loop, fixing the code is better. And scaling out costs money continuously while optimizing is a one-time effort, so you weigh those.

Senior

Why one Node process means one core for your JavaScript, how you actually measure requests-per-core, and what really caps a single instance's throughput.

Staff

How to plan capacity from measured headroom plus a safety margin, decide scale-out versus optimize by where the bottleneck lives, and spot the case where more instances make it worse.

Follow-up chain

  1. So one Node process runs your JavaScript on one core. What does that actually mean for how you use a multi-core machine?
  2. And how do you go about measuring that requests-per-core number the whole capacity plan hangs on?
  3. Say the event loop is the bottleneck on one instance. Why might adding instances do nothing for you, or even make things worse?
  4. You've got the option to either optimize the code or just add instances. How do you make that call?

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