Skip to main content
Madhukar
All Articles

Blocking vs Non-Blocking Code in Node.js

August 20, 20266 min read
JavaScriptNode.jsBackendWeb DevelopmentPerformance
Blocking vs Non-Blocking Code in Node.js

Waiting vs continuing — one line, two very different outcomes

Imagine a single cashier at a small shop. A blocking cashier serves one customer completely — ringing up every item, handling payment, bagging everything — before even looking up at the next person in line, no matter how long that takes. A non-blocking cashier starts a customer’s slower request (like a special order being prepared in the back), and immediately moves on to help the next person while that order gets ready, coming back to the first customer once it’s done.

In Node.js, this exact distinction — blocking versus non-blocking code — has a direct, measurable impact on how many users your server can actually serve well at the same time.

What Blocking Code Means

Blocking code stops the entire program from doing anything else until that specific operation finishes completely.

const fs = require("fs");

const data = fs.readFileSync("largeFile.txt", "utf8"); // blocks here
console.log("File read complete");
console.log("This line waits until the read above finishes");

fs.readFileSync is a synchronous (blocking) function — the entire Node.js process sits idle, doing nothing else, until the file finishes reading. Any other code, including handling other users' requests on a server, simply has to wait.

What Non-Blocking Code Means

Non-blocking code starts an operation and immediately continues running other code, without waiting for that operation to finish — coming back to handle the result once it’s actually ready.

const fs = require("fs");

fs.readFile("largeFile.txt", "utf8", (err, data) => {
console.log("File read complete");
});

console.log("This line runs immediately, without waiting");

fs.readFile (without Sync) is the asynchronous (non-blocking) version — it starts the file read, and the rest of the program keeps running immediately, with the callback firing later, once the read actually finishes.

Why Blocking Slows Servers

Node.js runs your JavaScript on a single thread — meaning it can only actively execute one piece of code at any given moment. This is fine, as long as that single thread never sits idle waiting on something slow.

Blocking code breaks that assumption directly. If one request triggers a blocking operation — a large synchronous file read, a slow synchronous calculation — every other request has to wait, even ones that have nothing to do with that specific operation.

In a small script run once, blocking code is often harmless — there’s nothing else waiting on that same thread. In a server handling many simultaneous users, blocking code becomes a genuine bottleneck: one slow request can stall every other user’s request behind it, even though they’re completely unrelated.

Async Operations in Node.js

Node.js’s built-in modules almost always provide both a blocking and non-blocking version of I/O operations — and the non-blocking version is the one meant for real server code.

// Blocking versions (avoid these in server code)
fs.readFileSync(...)
fs.writeFileSync(...)

// Non-blocking versions (use these instead)
fs.readFile(...)
fs.writeFile(...)

The non-blocking versions hand the actual slow work off to a background worker (as covered in this series’s article on how Node.js handles multiple requests with a single thread), letting the main thread stay free to keep handling other requests while that work completes.

This same blocking/non-blocking distinction applies broadly — file access, database queries, network requests — anywhere Node.js interacts with something outside its own immediate JavaScript execution.

Real-World Examples: File Reads and Database Calls

File reading: blocking vs non-blocking

// Blocking — freezes the server for every user while this one file reads
app.get("/report", (req, res) => {
const data = fs.readFileSync("largeReport.txt", "utf8");
res.send(data);
});

// Non-blocking — other requests keep being handled while this one waits
app.get("/report", (req, res) => {
fs.readFile("largeReport.txt", "utf8", (err, data) => {
res.send(data);
});
});

If largeReport.txt takes even half a second to read, the blocking version means every other user hitting the server during that half-second waits unnecessarily — even users requesting something completely different, like a homepage that doesn't touch this file at all.

Database calls

// Blocking-style thinking (most database drivers are async by default, avoid forcing this)
const user = getUserSync(userId); // would freeze the server while querying

// Non-blocking — the standard, correct approach
async function getUser(userId) {
const user = await db.users.findOne({ id: userId });
return user;
}

Database queries almost always involve real network or disk latency — exactly the kind of operation that should never block the main thread. Modern database drivers for Node.js are built to be non-blocking by default, returning promises that work naturally with async/await, covered in depth elsewhere in this series.

Final Takeaway

Blocking code freezes the entire single-threaded Node.js process until one specific operation finishes — harmless in a small script running alone, but a real bottleneck in a server juggling many simultaneous users, since every unrelated request gets stuck waiting behind it. Non-blocking code starts the same slow operation, but immediately frees up the thread to keep handling other work, coming back to the result once it’s ready. This is exactly why Node.js’s ecosystem consistently favors asynchronous, non-blocking versions of I/O operations — file access, database queries, network calls — as the default, correct choice for anything running inside a real server.

Frequently Asked Questions

Is fs.readFileSync always wrong to use?

> Not always — it’s genuinely fine for one-off scripts, command-line tools, or startup-time configuration loading, where nothing else is competing for the same thread. It becomes a real problem specifically inside request-handling code on a running server, where other users’ requests are waiting on that same thread.

How much does one blocking call actually slow down a server?

> It depends on how long the blocking operation takes and how many simultaneous users are affected — even a moderately slow blocking call (a few hundred milliseconds) can noticeably degrade responsiveness for every other concurrent user during that window, which compounds quickly under real traffic.

Are all asynchronous operations in Node.js automatically non-blocking?

> Generally, yes — Node.js’s asynchronous APIs are specifically designed to hand slow work off to background workers rather than blocking the main thread. The exception to watch for is heavy, synchronous CPU computation written directly in your own code, which can still block the thread regardless of whether I/O is involved.

Does using async/await guarantee my code is non-blocking?

> It depends on what you’re awaiting — await on a genuinely asynchronous operation (like a non-blocking database query) keeps things non-blocking. Awaiting something that's secretly synchronous and slow underneath doesn't magically make it non-blocking; the underlying operation itself needs to actually be asynchronous.

Blocking vs Non-Blocking Code in Node.js was originally published in Towards Dev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Originally published by Mr Madhukar

Read the complete article on Medium with full formatting & reader responses.