The Node Event Loop on a Server
In one line
Node serves thousands of concurrent connections on one JavaScript thread by never waiting for I/O — which means any code that does wait, or that computes for too long, blocks every other request in the process.
What it is
The loop runs phases in order: timers (setTimeout), pending callbacks, poll (where I/O completions arrive and where the loop parks when idle), check (setImmediate), and close callbacks. Between every callback it drains the microtask queue — promise continuations and process.nextTick, with nextTick running first. That last detail is a real footgun: a recursive process.nextTick starves the loop entirely, because the microtask queue must empty before the loop can advance.
Underneath sits libuv, and the important asymmetry lives there. Network I/O is genuinely asynchronous at the OS level — epoll, kqueue — so sockets scale without threads. Filesystem operations, DNS lookups via getaddrinfo, and crypto functions like pbkdf2 are not, so they run on a thread pool of four by default (UV_THREADPOOL_SIZE). Five concurrent fs.readFile calls means the fifth waits, and a slow DNS resolution can queue behind file reads in a way that looks impossible from the JavaScript side.
The failure mode on a server has no browser analogue: blocking is total. A 200ms JSON parse in one request adds 200ms of latency to every other request in flight, and the effect shows up as a tail-latency problem long before anyone suspects CPU. The usual culprits are large JSON.parse/JSON.stringify, synchronous crypto (bcrypt in sync mode), fs.readFileSync outside startup, catastrophic regex backtracking on user input (ReDoS — a security issue, not just a performance one), and per-request loops over big arrays.
Practical instrumentation: measure event loop lag (perf_hooks's monitorEventLoopDelay) and export it as a metric — it is the earliest and most reliable indicator that a Node service is in trouble, more so than CPU. For genuinely CPU-bound work, the answer is a worker thread or a separate service, not a cleverer async pattern; await doesn't yield to the loop in the middle of a synchronous computation.
Why it matters
Every Node performance question in a backend-flavoured interview reduces to this model, and the diagnostic story — "p99 latency degraded across all endpoints, event loop lag was 400ms, one endpoint was parsing a 10MB payload" — is the kind of concrete answer deep-dive rounds are looking for. It also explains the shape of Node's whole API surface.
Key points
- One thread runs your JavaScript, so a slow synchronous block delays every concurrent request, not just its own.
- Microtasks drain between callbacks, and
process.nextTickruns before promises — recursive use starves the loop. - Network I/O is truly async; filesystem, DNS and some crypto go through a four-thread pool you can resize.
- Event loop lag is the leading indicator of a struggling Node service and belongs on your dashboard.
async/awaitdoesn't make CPU work concurrent — it only yields at real await points.- ReDoS on user-supplied input blocks the whole process and is a denial-of-service vector, not just slow code.
- CPU-bound work belongs in a worker thread or another service; there is no async trick that fixes it in-loop.