Node.js Event Loop Blocked: Detect and Fix Slow Servers
One slow request freezes every other request in Node.js. Learn to measure event loop lag, find the blocking code, and fix it with streams, chunking, and worker threads.
When the Node.js event loop is blocked, a single slow synchronous operation freezes every other request on that process, so health checks time out and users see random slowness; detect it by measuring event loop delay, then move the blocking work to streams, smaller chunks, or worker threads.
The signature is confusing: CPU is high on one core, the database is idle, and unrelated endpoints are slow at the same time.
Why One Request Slows Everyone
Node runs your JavaScript on one thread. Asynchronous I/O is cheap, but any synchronous code runs to completion before the next request gets a turn. A 400 ms JSON.parse means every other request waits at least 400 ms.
Step 1: Measure Event Loop Delay
Node has a built-in histogram. No extra dependency needed.
import { monitorEventLoopDelay } from 'node:perf_hooks'
const h = monitorEventLoopDelay({ resolution: 20 })
h.enable()
setInterval(() => {
const p99 = h.percentile(99) / 1e6 // ms
logger.info({ eventLoopP99Ms: Math.round(p99), maxMs: Math.round(h.max / 1e6) }, 'loop-lag')
h.reset()
}, 10_000).unref()
Rough guide: p99 under 50 ms is healthy; consistently over 200 ms means users feel it. Alert on it the way you alert on error rate.
Step 2: Find the Blocking Code
node --cpu-prof server.js
# reproduce load, stop the process, open the .cpuprofile in Chrome DevTools
Or use clinic doctor or clinic flame. Look for wide flat bars on your own functions.
The Usual Suspects
| Culprit | Why it blocks | Fix |
|---|---|---|
JSON.parse / stringify on huge payloads |
Synchronous, scales with size | Paginate, stream (stream-json), limit body size |
fs.readFileSync, execSync |
Explicit sync I/O | Use async versions |
Sync crypto (pbkdf2Sync, scryptSync) |
CPU bound | Async variants or worker thread |
| Catastrophic regex | Exponential backtracking | Simplify regex, use a safe parser, cap input length |
Big array sort / map loops |
CPU bound | Chunk with setImmediate, move to worker |
| Template or PDF rendering | CPU bound | Worker or separate service |
Catastrophic regex deserves special care because attackers can trigger it deliberately with crafted input (ReDoS). Cap input length before matching.
Fix: Break Up Work
async function processAll(items) {
for (let i = 0; i < items.length; i++) {
heavy(items[i])
if (i % 500 === 0) await new Promise(setImmediate) // let other requests run
}
}
Fix: Use Worker Threads for CPU Work
// worker.js
import { parentPort, workerData } from 'node:worker_threads'
parentPort.postMessage(expensive(workerData))
import { Worker } from 'node:worker_threads'
export function runInWorker(data) {
return new Promise((resolve, reject) => {
const w = new Worker(new URL('./worker.js', import.meta.url), { workerData: data })
w.once('message', resolve)
w.once('error', reject)
})
}
For sustained load, use a worker pool such as piscina instead of spawning one per request.
Blocked Loops Look Like Other Problems
A frozen loop causes dropped connections (ECONNRESET), failed health checks that trigger restarts, and timeouts that look like database problems (connection pool exhaustion). It also inflates memory when requests queue up; compare with memory leak detection. Track per-route latency with p50/p95 per endpoint to see which route is responsible.
Let Relia Trace the Blocking Route
Relia monitors production latency and server load continuously and, when a route slows down, writes the incident up in plain words: what happened, why it thinks so (with a confidence level), and one thing to do next. On request it writes a fix plan, for example moving a hot loop to a worker or adding a payload limit, with a "Copy for your AI assistant" button; Relia doesn't change code itself.
FAQ
What is event loop lag?
The delay between when a timer or callback should run and when Node actually runs it. It rises when synchronous code hogs the thread.
Does async/await prevent blocking?
No. await only yields during real asynchronous I/O. A CPU-heavy loop inside an async function still blocks.
When should I use worker threads?
For CPU-bound tasks such as hashing, image processing, or large parsing. For I/O-bound work, normal async code is enough.