Blocking vs Non-Blocking Code in Node.js

Search for a command to run...

No comments yet. Be the first to comment.
You open WhatsApp, type a message, and hit send. A single grey tick appears. You're on a train with no signal, but the message went through anyway. Or did it? That single tick is the beginning of a su

You record a Reel, add a song, trim it, and then close the app before posting. When you reopen Instagram, the draft is right there waiting. Nothing was lost. That experience feels simple. The system b

If you have built a React Native app, you have felt the pain of navigation setup. It is easy to get wrong and hard to keep clean as apps grow. This article explains what routing means, why it matters,

Modern apps are not just a pile of screens. They are a living system of features, data flows, offline behavior, and performance constraints. If you have only shipped small apps, the jump to something

Your application has user accounts. Some routes should only be accessible to logged-in users. How do you protect routes and verify that a request actually comes from who they claim to be? JWT (JSON We

Ashish's Blog
57 posts
One fundamental concept separates great Node.js developers from frustrated ones: understanding blocking vs non-blocking code. Write blocking code, and your entire server freezes. Write non-blocking code, and it handles thousands of concurrent users.
Blocking code stops execution until an operation completes.
const fs = require('fs');
console.log('Starting');
const data = fs.readFileSync('file.txt'); // Waits here
console.log('File read');
console.log(data);
While readFileSync reads the file, nothing else happens. Your code is frozen. If this is a server handling requests, every other request waits.
Execution timeline:
0ms: Start
0ms: Call readFileSync
100ms: File read, execution continues
100ms: Log file content
The server is blocked for 100ms. If you have 100 simultaneous requests, they queue up for 100ms each. Total wait time: 10,000ms (10 seconds).
Non-blocking code delegates work and continues immediately:
import fs from 'fs';
console.log('Starting');
fs.readFile('file.txt', (err, data) => {
console.log('File read');
console.log(data);
});
console.log('Continuing');
Output:
Starting
Continuing
File read
[file content]
The file read happens in the background. Your code continues immediately.
Execution timeline:
0ms: Start
0ms: Delegate file read
0ms: Log "Continuing"
100ms: File read completes, callback executes
The server never blocks. It can handle other requests while waiting.
Let's simulate a server handling three requests, each requiring a 1-second file read:
Blocking Code:
app.get('/data', (req, res) => {
const data = fs.readFileSync('file.txt'); // Blocks for 1 second
res.send(data);
});
Timeline:
0s: Request 1 arrives, starts reading
1s: Request 1 done, Response 1 sent
1s: Request 2 arrives (was waiting), starts reading
2s: Request 2 done, Response 2 sent
2s: Request 3 arrives (was waiting), starts reading
3s: Request 3 done, Response 3 sent
Total time for all users: 3 seconds. Avg user wait: 2 seconds.
Non-Blocking Code:
app.get('/data', (req, res) => {
fs.readFile('file.txt', (err, data) => {
res.send(data);
});
});
Timeline:
0s: Request 1 arrives, delegates read, returns immediately
0s: Request 2 arrives, delegates read, returns immediately
0s: Request 3 arrives, delegates read, returns immediately
1s: All reads complete, all responses sent
Total time for all users: 1 second. Avg user wait: ~1 second.
Non-blocking is 3x faster with just 3 requests. With 100 requests, the difference is massive.
Common operations that have synchronous (blocking) versions:
import fs from 'fs';
// Blocking
fs.readFileSync('file.txt');
fs.writeFileSync('file.txt', 'data');
// Non-blocking alternatives
fs.readFile('file.txt', (err, data) => {});
fs.writeFile('file.txt', 'data', (err) => {});
Database operations:
// Blocking (bad)
const result = db.querySync('SELECT * FROM users');
// Non-blocking (good)
db.query('SELECT * FROM users', (err, result) => {});
HTTP requests:
// Blocking (bad - if such a function existed)
const response = http.getSync('https://api.example.com/data');
// Non-blocking (good)
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => console.log(data));
Some operations in Node.js are inherently slow:
Reading from disk (slower than RAM)
Network requests (very slow)
Database queries (slow)
Complex calculations (time-consuming)
Node.js can't speed these up. But it can handle them without blocking the thread by delegating to background workers.
Understanding the event loop explains the difference:
import fs from 'fs';
console.log('1');
fs.readFile('file.txt', () => {
console.log('2');
});
console.log('3');
Output:
1
3
2
Here's what happens:
1. JavaScript starts
- Logs "1"
- Delegates file read (tells worker thread)
- Logs "3"
- No more code
2. Event loop checks: Is anything ready?
- File read not done yet, nothing to do
3. Event loop checks: Is anything ready?
- File is ready! Execute callback
- Logs "2"
The callback waits in a queue until the main thread is free.
While callbacks are non-blocking, async/await is cleaner:
import fs from 'fs/promises';
// Using callbacks (non-blocking)
fs.readFile('file.txt', (err, data) => {
console.log(data);
});
// Using async/await (still non-blocking!)
const data = await fs.readFile('file.txt');
console.log(data);
Async/await looks like synchronous code but it's non-blocking. When you await a slow operation, the function pauses, but the thread continues handling other requests.
Bad: Synchronous (blocking)
import express from 'express';
import fs from 'fs';
const app = express();
app.get('/read-file', (req, res) => {
const data = fs.readFileSync('large-file.txt'); // Blocks entire server
res.send(data);
});
app.listen(3000);
Good: Asynchronous (non-blocking)
import express from 'express';
import fs from 'fs/promises';
const app = express();
app.get('/read-file', async (req, res) => {
try {
const data = await fs.readFile('large-file.txt');
res.send(data);
} catch (error) {
res.status(500).send('Error reading file');
}
});
app.listen(3000);
Bad: Database operations (blocking)
app.get('/users', (req, res) => {
const users = db.querySync('SELECT * FROM users'); // Blocks server
res.json(users);
});
Good: Database operations (non-blocking)
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
If even one route is blocking, it can impact others:
import express from 'express';
import fs from 'fs';
import fs_async from 'fs/promises';
const app = express();
// This route blocks the entire server
app.get('/slow', (req, res) => {
const data = fs.readFileSync('huge-file.txt'); // BLOCKS
res.send(data);
});
// This route is fast but blocked by /slow
app.get('/fast', (req, res) => {
res.send('Fast response');
});
// If someone calls /slow, requests to /fast must wait!
One blocking operation can bring down your entire server.
Even pure JavaScript computations can block:
// Blocks the thread for 5 seconds
function expensiveCalculation() {
let result = 0;
for (let i = 0; i < 1_000_000_000; i++) {
result += Math.sqrt(i);
}
return result;
}
app.get('/calculate', (req, res) => {
const result = expensiveCalculation(); // Blocks thread
res.json({ result });
});
For CPU-intensive work, use worker threads:
import { Worker } from 'worker_threads';
import path from 'path';
app.get('/calculate', (req, res) => {
const worker = new Worker('./worker.js');
worker.on('message', (result) => {
res.json({ result });
});
});
Mistake 1: Using readFileSync in a server
// Wrong - blocks entire server
app.get('/data', (req, res) => {
const data = fs.readFileSync('file.txt');
res.send(data);
});
// Right - non-blocking
app.get('/data', async (req, res) => {
const data = await fs.readFile('file.txt');
res.send(data);
});
Mistake 2: Not awaiting async operations
// Wrong - doesn't wait for query
app.get('/users', async (req, res) => {
const users = db.query('SELECT * FROM users');
res.json(users); // users is undefined
});
// Right - waits for query
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});
Mistake 3: Blocking in middleware
// Wrong - blocking middleware
app.use((req, res, next) => {
const user = db.querySync('SELECT * FROM users WHERE id=1');
req.user = user;
next();
});
// Right - non-blocking middleware
app.use(async (req, res, next) => {
req.user = await db.query('SELECT * FROM users WHERE id=1');
next();
});
Blocking code stops execution until operations complete
Non-blocking code delegates work and continues immediately
In a server, one blocking operation can block all other requests
Node.js provides non-blocking alternatives to synchronous functions
Use async/await for cleaner non-blocking code
Avoid .Sync methods (readFileSync, etc.) in servers
Even one blocking route can cripple server performance
CPU-intensive tasks should use worker threads
Embracing non-blocking code is what separates Node.js code that barely works from code that scales massively. Master this concept, and you've mastered Node.js.