Skip to main content

Command Palette

Search for a command to run...

Blocking vs Non-Blocking Code in Node.js

Published
7 min read
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/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.