I’ve been working with Node.js and Express for a few years now — building backend APIs, report generation systems, async job queues, and more. I used to proudly say, “Node.js is single-threaded!” but I never truly stopped to ask:
“Wait... if it’s just one thread, how is it handling multiple users, API calls, and DB queries at once — without getting stuck?”
So, I went down the rabbit hole. Here's what I learned, broken down with visuals and real-world examples — so even if you're new to backend dev, this will make sense.
🧠 TL;DR: Node.js = Single Thread + Event Loop + Smart Delegation
Node.js executes JavaScript in a single thread, but it doesn’t do everything by itself.
Instead, it delegates I/O tasks (like reading files, talking to the database, or calling APIs) to the OS or internal thread pool, and then picks them up when they’re done using something called the event loop.
It’s like being a restaurant manager who:
- Takes orders (JS code)
- Sends tasks to the kitchen (async I/O)
- Moves on to the next customer
- Serves dishes when the kitchen signals they’re ready
📦 Example in Express
Here’s a simple Express route:
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users.rows);
});
What happens here?
- The JS engine receives the request and starts executing the route handler.
- The
db.query
is asynchronous — it gets offloaded. - Node doesn’t wait. It continues handling other requests.
- Once the DB responds, the result is pushed into a callback queue.
- The event loop picks it up and continues from where it left off.
All of this happens on a single thread, but async tasks like the DB query are handled elsewhere until they’re ready.
🔁 Event Loop — The Conductor
The event loop is the mechanism that:
- Listens for completed tasks
- Queues callbacks
- Executes them when the main thread is free
Here are its phases (simplified):
Phase | What Happens |
---|---|
Timers | Runs setTimeout and setInterval callbacks |
Pending Callbacks | I/O callbacks that were deferred |
Idle, Prepare | Internal use |
Poll | Waits for incoming I/O events |
Check | Runs setImmediate
|
Close Callbacks | e.g., socket.on('close', ...)
|
⏱️ Tip: A
setTimeout(fn, 0)
doesn’t run immediately. It waits for the current event loop cycle to finish.
🧰 What is libuv
?
libuv
is a C++ library that powers Node.js under the hood. It provides:
-
A thread pool (default 4 threads) for things like:
- File system access
- DNS lookups
- Compression
- Crypto
Handles cross-platform compatibility (Windows, Linux, macOS)
Manages the event loop
This is how Node.js, despite being “single-threaded,” can still do background work — it delegates those tasks to libuv
’s thread pool or the OS.
🧪 Real-World Analogy: A Restaurant Waitlist System
Imagine you're at a popular restaurant that’s always busy. Here’s how it works:
- There’s one host at the front desk (Node.js main thread).
- The host takes your name and party size, then adds you to the waitlist (callback/event queue).
- Instead of standing there doing nothing, the host keeps greeting and adding new guests.
- Meanwhile, tables (resources) are getting freed up in the dining area (asynchronous tasks completing).
- When a table is ready, the system alerts the host, and the host calls the next guest from the waitlist.
No one is blocking the doorway. The host is constantly working, and everyone eventually gets seated.
In this analogy:
- The host = main thread
- The waitlist system = event loop
- Tables becoming available = async I/O finishing
- Guests being called in = callbacks getting executed
Just like Node.js, the host doesn't multitask — they just delegate, stay non-blocking, and handle what's ready next.
🧨 What Can Go Wrong?
❌ Blocking Code
const data = fs.readFileSync('./bigfile.json');
This blocks the main thread. No other code can run until this finishes — not ideal for a high-traffic app.
Use this instead:
fs.readFile('./bigfile.json', (err, data) => {
// non-blocking
});
Or the Promise
version:
const data = await fs.promises.readFile('./bigfile.json');
❌ Heavy CPU Work
Stuff like:
- Image processing
- Large data transformations
- Video encoding
These will choke the single thread.
🛠 Solution: Offload to a worker thread, child process, or move it to a microservice.
🧠 Summary
- Node.js runs JS in a single thread.
- It uses an event loop to manage callbacks and async operations.
- I/O-heavy tasks are delegated to the OS or a thread pool (
libuv
). - This is why Node can handle thousands of concurrent users without blocking.
🧳 My Takeaway
After years of using Node and Express in real projects — from API to detailed reports generation — I finally understood what makes Node.js special.
It’s not just about non-blocking code. It’s about knowing when your code blocks, and how the event loop, thread pool, and system play together.
If you’re building backends, understanding this gives you superpowers — better code, better scalability, and fewer random performance issues.
Top comments (0)