Skip to main content

Command Palette

Search for a command to run...

Blocking vs Non-Blocking Code in Node.js

Node.js is built for non-blocking I/O, so your app can do more in less time.

Updated
6 min read
Blocking vs Non-Blocking Code in Node.js
R

Software engineer passionate about tech, innovation & research. I explore, build, and share insights on coding, systems, and emerging technologies.

Introduction

Understanding blocking and non-blocking behavior is fundamental to writing efficient Node.js applications. Node.js operates on a single-threaded event loop, which means that the way code is executed directly affects performance, scalability, and responsiveness. This document explains the concepts in a structured and practical manner, focusing on real-world implications and correct mental models.

What Blocking Code Means

Blocking code is any code that prevents the JavaScript thread from executing further instructions until the current operation completes. During this time, the event loop is effectively paused.

Characteristics of Blocking Code

  • Execution halts until the operation finishes

  • The call stack remains occupied

  • No other callbacks or requests can be processed

  • Common in synchronous APIs

Example

const fs = require("fs");

console.log("Start");

const data = fs.readFileSync("data.txt", "utf8"); // blocking

console.log("File length:", data.length);
console.log("End");

Behavior

  • The thread stops at readFileSync

  • File must be fully read before execution continues

  • Any incoming requests or operations must wait

Key Insight

Blocking code is not inherently wrong, but it is dangerous in environments where concurrency is required, such as servers handling multiple users.

What Non-Blocking Code Means

Non-blocking code initiates an operation and immediately returns control to the event loop. The operation completes in the background, and a callback or promise handles the result later.

Characteristics of Non-Blocking Code

  • Execution continues immediately

  • Background systems handle slow operations

  • Results are handled asynchronously

  • Enables concurrency

Example

const fs = require("fs");

console.log("Start");

fs.readFile("data.txt", "utf8", (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log("File length:", data.length);
});

console.log("End");

Behavior

  • File reading starts but does not block execution

  • "End" logs before file content is processed

  • Callback executes later when file is ready

Key Insight

Non-blocking code allows Node.js to handle multiple operations efficiently without waiting.

Why Blocking Slows Servers

Node.js uses a single thread to handle all incoming requests. If that thread is blocked, the entire server becomes unresponsive.

Scenario: Blocking Server

const http = require("http");
const fs = require("fs");

http.createServer((req, res) => {
  const data = fs.readFileSync("large.json"); // blocks
  res.end(data);
}).listen(3000);

Behavior

  • Each request waits for file read to complete

  • Requests are processed sequentially

  • High latency under load

Scenario: Non-Blocking Server

const http = require("http");
const fs = require("fs");

http.createServer((req, res) => {
  fs.readFile("large.json", (err, data) => {
    if (err) {
      res.statusCode = 500;
      return res.end("Error");
    }
    res.end(data);
  });
}).listen(3000);

Behavior

  • Multiple file reads start simultaneously

  • OS handles operations in parallel

  • Server remains responsive

Performance Impact

Blocking model:

  • Requests handled one at a time

  • Throughput decreases

  • Latency increases linearly

Non-blocking model:

  • Requests overlap in execution

  • High concurrency

  • Better resource utilization

Async Operations in Node.js

Node.js handles asynchronous operations using the event loop and background systems such as libuv.

How It Works

  1. Async operation is initiated

  2. Operation is offloaded to the system or thread pool

  3. Callback or promise is registered

  4. Execution continues immediately

  5. When operation completes, callback is queued

  6. Event loop executes callback when call stack is empty

Example with Promises

const fs = require("fs").promises;

async function readFile() {
  const data = await fs.readFile("data.txt", "utf8");
  console.log(data.length);
}

readFile();

Important Note

  • async/await does not make code non-blocking

  • It only provides cleaner syntax over promises

  • The underlying operation must still be asynchronous

Real-World Examples

File Read Comparison

Blocking

const fs = require("fs");

const data = fs.readFileSync("file.txt", "utf8");
console.log(data);

Non-Blocking

const fs = require("fs");

fs.readFile("file.txt", "utf8", (err, data) => {
  console.log(data);
});

Multiple File Reads

Blocking Approach

const fs = require("fs");

const a = fs.readFileSync("a.txt");
const b = fs.readFileSync("b.txt");
const c = fs.readFileSync("c.txt");

Total time: Time(A) + Time(B) + Time(C)

Non-Blocking Approach

const fs = require("fs");

fs.readFile("a.txt", () => console.log("A done"));
fs.readFile("b.txt", () => console.log("B done"));
fs.readFile("c.txt", () => console.log("C done"));

Total time: Approximately Time(slowest file)

Database Example

async function getUsers(db) {
  const users = await db.query("SELECT * FROM users");
  return users;
}
  • Query runs asynchronously

  • Event loop remains free

  • Other requests are processed simultaneously

Waiting vs Continuing Execution Analogy

Consider a single worker handling customer requests.

Blocking Model

  • Worker takes a request

  • Waits until task completes

  • Only then takes next request

Non-Blocking Model

  • Worker delegates task to another system

  • Immediately handles next request

  • Returns to previous task when result is ready

Insight

The difference is not speed of execution, but whether time is wasted waiting or used productively.

File Handling Scenario Comparison

Blocking Timeline

Time →  |----Read File A----|----Read File B----|

Request A → Completed after full read
Request B → Waits for A, then processed

Non-Blocking Timeline

Time →  |--Start A--\        /--Finish A--|
        |--Start B--\--overlap--/--Finish B--|

Request A → Completes independently
Request B → Completes independently

Key Difference

Blocking forces sequential execution Non-blocking enables overlapping operations

Impact on Server Performance

Blocking

  • High response time under load

  • Poor scalability

  • Requests queue up

  • Increased memory usage

Non-Blocking

  • Low latency

  • High concurrency

  • Efficient CPU utilization

  • Scales to thousands of connections

Example Scenario

100 concurrent users:

Blocking:

  • Each request waits for previous

  • Total delay accumulates

Non-blocking:

  • All requests initiated immediately

  • Responses handled as ready

Diagrams

Blocking Execution Timeline

Request 1: |====Processing====|
Request 2:             |====Processing====|
Request 3:                        |====Processing====|

Non-Blocking Execution Timeline

Request 1: |====Waiting====|==Done==|
Request 2:   |====Waiting====|==Done==|
Request 3:     |====Waiting====|==Done==|

Conceptual Flow

Call Stack → Executes code
Async Task → Offloaded
Task Queue → Stores callback
Event Loop → Moves callback to stack

Suggestions and Best Practices

Avoid Blocking APIs

Do not use synchronous methods in request handlers:

  • fs.readFileSync

  • fs.writeFileSync

  • crypto.*Sync

Use Async Patterns

Prefer:

  • Callbacks

  • Promises

  • async/await

Use Streams for Large Data

Streams process data in chunks:

  • Reduce memory usage

  • Improve performance

Handle CPU-Intensive Work Properly

  • Use worker threads

  • Break tasks into smaller chunks

  • Avoid long-running loops on main thread

Monitor Performance

  • Profile application

  • Identify blocking operations

  • Optimize bottlenecks

Keep Handlers Lightweight

  • Delegate heavy work

  • Use background processing when needed

Conclusion

Blocking and non-blocking behavior defines how well a Node.js application performs under load. Blocking code halts the event loop and prevents concurrency, while non-blocking code allows the system to handle multiple operations efficiently.

The difference is not just technical—it directly impacts user experience, scalability, and reliability. Writing non-blocking code is not optional in Node.js; it is essential for building production-grade systems.