In short
Reading a file or querying a database takes forever in CPU time. Node doesn't stand and wait: it starts the job, serves other users, and comes back when the result is ready.
Node's approach of starting an I/O operation (file read, network call) and continuing to execute other code while it completes in the background, instead of waiting.

Think of it like this
The one-waiter dhaba
One waiter runs the whole dhaba. He takes table 1's order, passes the slip to the kitchen, and goes straight to table 2 instead of standing by the stove. When a dish is ready, the kitchen calls out and he serves it. One waiter, many happy tables.
Watch it happen
Two users, one thread
GET /report
1/6User A asks for a report that needs a large file from disk.
The big ideas
What you need to know
I/O is mostly waiting
Disk reads, database queries and API calls spend most of their time waiting on something outside your code.
Sync calls block everyone
readFileSync and friends make the whole process wait. Fine at startup, harmful inside request handlers.
libuv does the waiting
Network I/O uses the OS's own async features. File work goes to a small thread pool, four threads by default.
Great for I/O, not for CPU
Node shines at many slow-ish I/O tasks. Heavy maths or image processing needs worker threads or another service.
See it in code
Walk through the code
1import http from "node:http";2import { readFileSync } from "node:fs";3import { readFile } from "node:fs/promises";4 5http.createServer(async (req, res) => {6 if (req.url === "/sync") {7 const data = readFileSync("big.csv"); // every other user waits8 return res.end(`read ${data.length} bytes\n`);9 }10 if (req.url === "/async") {11 const data = await readFile("big.csv"); // others keep being served12 return res.end(`read ${data.length} bytes\n`);13 }14 res.end("ok\n");15}).listen(3000);1/3readFileSync freezes the thread until the whole file is read. Every other request waits in line.
Create a big.csv of a few hundred MB first. Hit /sync and, at the same time, hit / from a second terminal. Repeat with /async and compare.
Save yourself the pain
Common mistakes
Oops
Using fs.readFileSync or other *Sync calls inside request handlers.
Do this instead
Sync calls are fine at startup, like loading config. Inside handlers, use the promise versions from node:fs/promises.
Oops
Thinking non-blocking means Node runs your JavaScript in parallel.
Do this instead
Your JavaScript still runs on one thread. Only the waiting, for disk or network, happens in the background.
Your mission
Feel the block
Quick check
A Node server calls await db.query() and the query takes 200 ms. What is the main thread doing during those 200 ms?
Go deeper
- 1Compare a blocking `readFileSync` call to a non-blocking `readFile` callback version.
- 2Learn how libuv's thread pool handles I/O operations behind the scenes.
- 3Understand why this model lets Node handle high concurrency with a single thread.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.