Blocking vs Non-Blocking Code in Node.js
Understand how blocking and non-blocking code works in Node.js, why it matters for performance, and how it impacts real server behavior.

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.
Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/
Connect with me on X for quick updates and insights: @abdul_sama60108
One of the first things that trips up developers new to Node.js is understanding why some code patterns are considered "bad" even when they technically work. You read a file, you get the data, everything looks fine. But under real server load, that same code can bring your application to its knees.
The difference comes down to one concept: blocking vs non-blocking code. Understanding this isn't just theoretical it directly affects how your server behaves when real users are hitting it. Let's break it down clearly.
What Blocking Code Means
Blocking code is code that stops everything else from running until it finishes. When a blocking operation is in progress, the entire Node.js thread freezes and waits. No other requests get processed. No other code runs. Everything halts.
The name is literal it blocks the thread.
const fs = require('fs');
console.log("Before reading file");
// Synchronous (blocking) file read
const data = fs.readFileSync('notes.txt', 'utf8');
console.log("File content:", data.trim());
console.log("After reading file");
Expected Output:
Before reading file
File content: These are my notes.
After reading file
This looks perfectly fine in isolation. The file is read, the content is printed, life goes on. But notice what readFileSync does the Sync in the name tells you everything. It's synchronous. Node.js stops at that line, reads the entire file, and only then moves to the next line.
For a single script running once, this is harmless. On a server handling multiple users simultaneously, it's a serious problem.
What Non-Blocking Code Means
Non-blocking code does the opposite it initiates an operation and immediately moves on, without waiting for the result. When the operation finishes, a callback (or a promise) handles the result. The thread is never frozen. It's always available to do other work.
const fs = require('fs');
console.log("Before reading file");
// Asynchronous (non-blocking) file read
fs.readFile('notes.txt', 'utf8', function (err, data) {
if (err) {
console.log("Error:", err.message);
return;
}
console.log("File content:", data.trim());
});
console.log("After reading file");
Expected Output:
Before reading file
After reading file
File content: These are my notes.
"After reading file" prints before the file content even though the file read comes first in the code. That's the non-blocking behavior. Node.js fired off the file read request, immediately continued to the next line, and came back to handle the file data when it was ready.
The thread was never frozen. It was always free.
Why Blocking Slows Servers
In isolation, blocking code runs just fine. The problem surfaces the moment more than one user is involved which is the entire point of a server.
Node.js runs on a single thread. That one thread handles all incoming requests. If that thread gets blocked by one slow operation a large file read, a slow database query every other request that arrives during that time has to wait. Not because the server is busy computing something but because it's frozen, doing nothing, waiting for data to come back.
Imagine a single bank teller who, after taking your request, disappears into the back office to process your paperwork and refuses to acknowledge anyone else until they return. Every customer who arrives while they're gone just stands at the counter waiting. That's blocking behavior on a server.
const http = require('http');
const fs = require('fs');
const server = http.createServer(function (req, res) {
console.log(`Request received at: ${new Date().toISOString()}`);
// Blocking operation inside a server handler dangerous
const data = fs.readFileSync('largefile.txt', 'utf8');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(data);
});
server.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (in terminal):
Server running on port 3000
Request received at: 2024-01-15T10:30:00.000Z
This server works for one user. But if largefile.txt takes 200ms to read, every other request that arrives in those 200ms is completely frozen not queued intelligently, just blocked. Under real traffic, this pattern cascades into unacceptable response times.
Async Operations in Node.js
Node.js was built from the ground up to handle async operations efficiently. When you use non-blocking APIs, Node.js hands the work off to the underlying system the OS handles the file read or network call in the background and the JavaScript thread stays free to handle other requests.
When the background operation finishes, the result is placed in the task queue, and the event loop picks it up and runs the callback when the thread is available.
This means the thread is always doing useful work instead of waiting. It's the core reason Node.js can handle high concurrency on a single thread.
const http = require('http');
const fs = require('fs');
const server = http.createServer(function (req, res) {
console.log(`Request received at: ${new Date().toISOString()}`);
// Non-blocking operation thread stays free
fs.readFile('largefile.txt', 'utf8', function (err, data) {
if (err) {
res.writeHead(500);
res.end('Error reading file');
return;
}
console.log(`Response sent at: ${new Date().toISOString()}`);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(data);
});
// Thread is already free here can handle next request
});
server.listen(3000, function () {
console.log('Non-blocking server running on port 3000');
});
Expected Output (with multiple requests arriving):
Non-blocking server running on port 3000
Request received at: 2024-01-15T10:30:00.000Z
Request received at: 2024-01-15T10:30:00.050Z
Request received at: 2024-01-15T10:30:00.100Z
Response sent at: 2024-01-15T10:30:00.210Z
Response sent at: 2024-01-15T10:30:00.230Z
Response sent at: 2024-01-15T10:30:00.245Z
All three requests were received nearly instantly the thread wasn't blocked between them. The responses arrived shortly after as each file read completed in the background. That's the difference in real terms.
Real-World Examples: File Read and DB Calls
Seeing the pattern in concrete, practical scenarios is what makes it stick. These two situations file reading and database calls are where the blocking vs non-blocking choice shows up most often in real Node.js applications.
File Read: Blocking vs Non-Blocking
const fs = require('fs');
// ❌ Blocking approach
function readUserFileBlocking() {
console.log("Reading file (blocking)...");
const data = fs.readFileSync('user.txt', 'utf8');
console.log("User data:", data.trim());
console.log("This line waited for the file.");
}
// ✅ Non-blocking approach
function readUserFileNonBlocking() {
console.log("Reading file (non-blocking)...");
fs.readFile('user.txt', 'utf8', function (err, data) {
if (err) return console.log("Error:", err.message);
console.log("User data:", data.trim());
});
console.log("This line ran without waiting for the file.");
}
readUserFileNonBlocking();
Expected Output:
Reading file (non-blocking)...
This line ran without waiting for the file.
User data: John Doe, age 28
Database Call: Simulating Blocking vs Non-Blocking Behavior
In real applications, database queries are one of the most common sources of blocking behavior if handled incorrectly. While Node.js database drivers are async by default, understanding the pattern matters.
// Simulating async DB call with setTimeout
function getUserFromDB(userId, callback) {
console.log(`Fetching user ${userId} from database...`);
// Simulating a DB query that takes 150ms
setTimeout(function () {
const user = { id: userId, name: "Jane Doe", role: "admin" };
callback(null, user);
}, 150);
}
console.log("Server handling request...");
getUserFromDB(42, function (err, user) {
if (err) return console.log("DB Error:", err.message);
console.log("User retrieved:", user.name, "| Role:", user.role);
});
console.log("Thread is free can handle other requests while DB responds");
Expected Output:
Server handling request...
Fetching user 42 from database...
Thread is free can handle other requests while DB responds
User retrieved: Jane Doe | Role: admin
The DB call takes 150ms but the thread doesn't freeze for those 150ms. It moves on, stays available, and handles the result when it arrives. In a real server under load, those 150ms of thread availability could mean the difference between handling 10 requests and handling 500.
Wrapping Up
Blocking vs non-blocking isn't a subtle technical distinction it's a fundamental design decision that determines how your server behaves under pressure. Let's pull it all together:
Blocking code freezes the Node.js thread until an operation finishes. Fine for scripts, dangerous for servers.
Non-blocking code initiates an operation and moves on immediately, handling the result through a callback when it's ready.
Blocking slows servers because Node.js is single-threaded one frozen thread means every other user waits.
Async operations in Node.js offload work to the OS and stay thread-free, enabling high concurrency without multiple threads.
File reads and DB calls are the most common real-world scenarios where this choice matters most.
The rule of thumb for Node.js server code is straightforward: if there's an async version of an operation, use it. readFile over readFileSync, async database drivers over synchronous ones. Keep the thread moving, keep the server responsive, and your application will hold up when it matters.
FIN ✌️




