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.

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.
Keep reading

401 vs 403, the difference in one request
401 means the server doesn't know who you are; 403 means it does, and the answer is no. Watch one request hit both gates.

Express middleware explained, step by step
Middleware is a line of functions every request walks before your handler. Watch three stations stamp one request in order, then a missing token stop it early.

How the Node event loop works, visually
Node runs on one thread yet never blocks. Watch one async callback wait in the queue and get pushed onto the call stack to see how.