NodeJSBlocking vs Non-Blocking Code

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

readFileSync, big for loops, JSON.parse of huge data

readFile, fetch, await query()

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 …Sync function (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

JS
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)
One blocked request blocks EVERY other request
Hit `/block` and then `/` in another tab: the fast route hangs for the full 5 seconds, because the single thread is stuck in the `while` loop and the event loop can't dispatch anything else. In a real server, one slow synchronous operation degrades latency for *all* concurrent users. This is the cardinal sin of Node.
Sync vs async I/O, side by side

JS
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 meantime
Sync APIs aren't evil — context matters
`readFileSync` is perfectly fine in a one-off CLI script, a build tool, or **startup code** that runs once before the server accepts traffic (loading config, reading a cert). The rule is narrower than "never": don't block inside the *request-handling hot path* of a long-running server, where it steals time from concurrent work.
Fixing 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

JS
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

JS
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:

JS
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)
Common hidden blockers
Beyond obvious loops, watch for: `JSON.parse`/`JSON.stringify` on large payloads, synchronous `crypto` (`pbkdf2Sync`, big `scryptSync`), `zlib` sync methods, huge regex (catastrophic backtracking), and `fs.*Sync` in request handlers. Each silently blocks the loop. Prefer the async variants, stream large data, and move CPU work to workers.
The rules
  • Prefer async I/O (readFile, fs/promises) over …Sync anywhere 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.

Next
Back to the syntax of async — start with the original convention every Node API was built on: [The Callback Pattern](/nodejs/callback-pattern).