Blocking vs Non-Blocking Code in Node.js

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.
What Is Blocking Code?
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).
What Is Non-Blocking Code?
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.
Real-World Impact
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.
Blocking Operations in Node.js
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));
Why Does Blocking Happen?
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.
The Event Loop Revisited
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.
Async/Await: Cleaner Syntax
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.
Practical Examples
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);
});
Mixing Blocking and Non-Blocking
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.
CPU-Intensive Code
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 });
});
});
Common Mistakes
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();
});
Key Takeaways
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/awaitfor cleaner non-blocking codeAvoid
.Syncmethods (readFileSync, etc.) in serversEven 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.




