Blocking vs Non-Blocking Code
Node's whole performance story rests on not blocking the single thread. Blocking code holds the thread until it finishes — and while it does, nothing else in your entire process can run. Non-blocking code starts an operation and returns immediately, letting the event loop keep serving everyone else. Knowing how to tell them apart, and how to fix blocking, is core Node competence.
The definition
Blocking | Non-blocking | |
|---|---|---|
Returns | Only when the work is done | Immediately; result comes later |
During the wait | Thread frozen — nothing else runs | Thread free for other work |
Examples |
|
|
Cost of misuse | Whole server stalls | — |
The two kinds of blocking
They're often conflated, but the fixes differ:
Sync I/O blocking — calling a
…Syncfunction (readFileSync,execSync). The thread waits on the disk/network. Fix: use the async version.CPU blocking — a long computation (image resize, parsing, crypto, a million-iteration loop). The thread is busy computing. Fix: offload to a worker thread or break it up.
Watch a server freeze
import { createServer } from 'node:http'
createServer((req, res) => {
if (req.url === '/block') {
// CPU-blocking: this loop owns the thread for seconds
const end = Date.now() + 5000
while (Date.now() < end) {}
res.end('done blocking')
} else {
res.end('fast') // but THIS can't respond while /block runs!
}
}).listen(3000)Sync vs async I/O, side by side
import { readFileSync, readFile } from 'node:fs'
// BLOCKING — thread frozen until the file is fully read
const data = readFileSync('big.log', 'utf8')
process(data)
// NON-BLOCKING — returns now; callback fires when ready
readFile('big.log', 'utf8', (err, data) => process(data))
// the thread is free to handle other requests in the meantimeFixing CPU-bound blocking
Async I/O won't help a heavy computation — there's no I/O to delegate; the thread genuinely has to do the math. The answer is a worker thread, which runs the work on a separate thread and posts back the result, leaving the main loop free:
main.js — offload, don't block
import { Worker } from 'node:worker_threads'
function heavyTask(input) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: input })
worker.once('message', resolve)
worker.once('error', reject)
})
}
// The main thread keeps serving requests while the worker computes:
const result = await heavyTask({ size: 1e9 })worker.js — runs off the main thread
import { parentPort, workerData } from 'node:worker_threads'
let sum = 0
for (let i = 0; i < workerData.size; i++) sum += i // heavy, but isolated
parentPort.postMessage(sum)Detecting blocking
If your app feels intermittently unresponsive, measure event-loop lag — how late timers fire compared to schedule. Growing lag means something is hogging the thread:
import { monitorEventLoopDelay } from 'node:perf_hooks'
const h = monitorEventLoopDelay()
h.enable()
setInterval(() => {
console.log('loop delay p99 (ms):', (h.percentile(99) / 1e6).toFixed(1))
}, 1000)The rules
Prefer async I/O (
readFile,fs/promises) over…Syncanywhere a server is serving traffic.Move CPU-heavy work to
worker_threads(or a separate service / queue).Keep each task short so the event loop keeps turning.
Sync APIs are fine in scripts and startup, not in the request hot path.
Measure loop lag to catch blocking before users do.