Skip to content
ExplainerBackend

Blocking vs non blocking in Node, watch the thread pool

One readFileSync freezes every user on your server. Watch the thread stop on one line, then hand the read to a worker and answer everyone else meanwhile.

3 min read
Blocking vs non blocking in Node, watch the thread pool, the opening step of the interactive scene

One thread runs your JavaScript

Every request is answered by one thread with one call stack. Whatever is on that stack runs to the end before anything else can.

Step 1 of 6

"Node is single threaded" and "Node is non blocking" are both true, and the word that reconciles them is Sync. Drop that suffix and the read you were doing on the one thread goes to a worker instead. Step through the scene above to watch one gigabyte freeze five users, then watch the same read happen somewhere else.

Blocking means the thread does the work itself

Your JavaScript runs on a single thread with a single call stack. Whatever is on the stack runs to the end before anything else can, and a request handler is just another thing on that stack.

app.get("/big", (req, res) => {
  const file = fs.readFileSync("./big.bin");   // the thread reads. All of it.
  res.send(file);
});

readFileSync means "do not go past this line until the whole file is in memory". For a two kilobyte config file that is a millisecond and you will never notice. For a gigabyte it is tens of seconds, and for the whole of that time the call stack holds one line, for one user. The event loop is still turning; it just has nowhere to put the next request. Users two, three, four and five sent a millisecond of work each and get nothing until user one's read finishes, and then they all burst through at once.

That is the shape of every "my server randomly hangs" bug. The server is not slow. It is frozen, on one line, and the line is usually one with Sync on the end.

Non blocking means someone else does the work

Same handler, one word gone:

app.get("/big", (req, res) => {
  fs.readFile("./big.bin", (err, file) => {   // returns at once
    res.send(file);                            // runs when the read is done
  });
});

readFile hands the read to one of libuv's worker threads, a small pool the runtime keeps beside your JavaScript, and returns in a microsecond. The call stack is empty again, so the loop runs the next handler, and users two to five are answered while the gigabyte is still coming off the disk. When the worker finishes, it queues your callback, and the loop runs it the next time the stack is clear. User one waited on the read, and only on the read.

Two things worth knowing on sight. The pool is four threads by default (raise it with UV_THREADPOOL_SIZE if you have a good reason), and it serves file system work and DNS, not sockets: network I/O goes straight to the operating system, which is how one process holds thousands of connections. And the rule that falls out of all this is short: never call a Sync function while a server is handling requests. The Node guide on not blocking the event loop lists every call that does; the lesson lets you drag the file size up, run both servers against five users, and watch the total wait.

Common questions

Is Node single threaded or not?
Your JavaScript runs on one thread, always. Beside it, libuv keeps a small pool of worker threads (four by default) for file reads, DNS lookups and a few other jobs, and the operating system handles network sockets. So the runtime has several threads; your code has one.
Why is readFileSync fine at startup but not in a handler?
At startup nobody is connected, so a frozen thread costs nothing. Once requests are flowing, that same freeze holds every user at once. Read config synchronously before `listen()`, and never inside a route.
Does async/await make my code non blocking?
Only if the thing you await is itself non blocking. `await fs.promises.readFile()` hands the read to the pool and frees the thread. `await` in front of a long `for` loop changes nothing, because the loop still runs on the main thread.
Do HTTP requests to other services use the thread pool?
No. Sockets go through the operating system's own async mechanisms (epoll, kqueue, IOCP), which is why a server can hold thousands of open connections without a thread each. The pool is for file system work, DNS and a handful of CPU heavy library calls.