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
- 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?
- Say the flame graph shows your handler is cheap, but requests are still slow. Where's that time hiding?
- How would you pull a CPU profile off a live production process without taking it down?
- So you found the hot function and optimized it. How do you prove that actually helped?