Skip to main content

Command Palette

Search for a command to run...

Blocking vs Non-Blocking Code in Node.js

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

Full-stack developer with a good foundation in frontend, now specializing in backend development. Passionate about building efficient, scalable systems and continuously sharpening my problem-solving skills. Always learning, always evolving.

A few months into running a Node.js API in production, you notice something odd. CPU usage looks fine, memory is stable, yet requests start timing out under load. You add more logs, profile the system, and eventually realize the problem is not scale. It is how your code executes. Some operations quietly block everything, while others let the server breathe.

That distinction is the difference between a responsive system and a struggling one.


What Blocking Code Means

At a low level, blocking is about execution flow on the main thread.

Node.js runs JavaScript on a single thread. When that thread is executing a function, nothing else can run until it finishes.

A blocking operation:

  • Starts execution

  • Holds the thread

  • Prevents any other work from progressing

  • Only releases control after completion

In practical terms:

const data = fs.readFileSync('large.txt', 'utf8');
console.log(data);

This call does not return until the file is fully read. During that time:

  • No other request handler runs

  • No callbacks are processed

  • The event loop is effectively paused

This aligns with how Node defines blocking behavior. The thread waits for the operation to complete before moving forward. (Node.js)

Why this matters

In a server context, that means:

  • One slow operation can delay unrelated requests

  • Latency stacks up under load

  • Throughput drops even if CPU is underutilized


What Non-Blocking Code Means

Non-blocking code flips that model.

Instead of waiting, the operation:

  • Starts

  • Delegates work elsewhere

  • Immediately returns control to the main thread

Example:

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

Here is what actually happens:

  1. Node registers the file read request

  2. The operation is handed off to the system or worker pool

  3. The main thread continues executing other code

  4. When done, a callback is queued and executed later

This is the essence of non-blocking I O. The program continues execution while the operation runs in the background. (GeeksforGeeks)

Important nuance

Non-blocking does not mean parallel JavaScript execution.

It means:

  • JavaScript remains single-threaded

  • I O work happens outside that thread

  • Results come back asynchronously via callbacks, promises, or async await


Why Blocking Slows Servers

This is where theory turns into production impact.

Node’s scalability depends on one assumption:

The event loop must stay free to process new events.

Blocking breaks that assumption.

What actually happens under load

Imagine 100 incoming requests:

  • Each needs to read a file or query a database

  • If you use blocking operations:

    • Request 1 blocks the thread

    • Requests 2 to 100 wait in line

  • If each request takes 100ms:

    • Total time becomes roughly 10 seconds

That is not concurrency. That is serialization.

Blocking code prevents the event loop from handling other requests, creating a bottleneck even when resources are available. (Upsun Developer)

Key impact areas

  • Throughput drops because requests are processed sequentially

  • Latency increases because each request waits its turn

  • User experience degrades under load, not necessarily at low traffic


How Async Operations Work in Node.js

To understand non-blocking properly, you need to understand what Node actually does under the hood.

Core components

  • Event Loop: The scheduler that runs JavaScript callbacks

  • libuv: The system layer handling I O and worker threads

  • Callback Queue: Where completed async tasks wait for execution

The event loop enables Node.js to perform non-blocking I O even though JavaScript runs on a single thread. (Node.js)

Execution flow

  1. Your code initiates an async operation

  2. Node delegates it:

    • File system → worker thread pool

    • Network I O → OS-level async APIs

  3. The main thread continues processing other requests

  4. When the operation completes:

    • A callback or promise resolution is queued
  5. The event loop executes it when the stack is free

This delegation model is why Node can handle many concurrent operations without spawning threads per request. (Medium)

Critical constraint

Async I O is non-blocking. CPU work is not.

If you do this:

fs.readFile('file.txt', () => {
  // CPU-heavy loop
  while (Date.now() < Date.now() + 200) {}
});

You are still blocking the event loop.

CPU-heavy work blocks execution regardless of async context. (NodeSource)


Real-World Examples

1. File System Operations

Blocking version

const data = fs.readFileSync('large.txt', 'utf8');
res.send(data);
  • Request blocks until file is read

  • Other requests cannot proceed

Non-blocking version

fs.readFile('large.txt', 'utf8', (err, data) => {
  res.send(data);
});
  • File read happens in background

  • Server continues handling other requests

2. Database or API Calls

Blocking pattern (conceptual)

const result = db.querySync('SELECT * FROM users');
  • Entire server waits for DB response

Non-blocking pattern

const result = await db.query('SELECT * FROM users');
  • Query runs asynchronously

  • Event loop stays free for other work

Important clarification

await looks synchronous, but it is not blocking the thread. It pauses the function, not the event loop.


Mental Model: Waiting vs Continuing

A useful way to reason about this:

  • Blocking: You place an order and stand at the counter until it is ready

  • Non-blocking: You place an order, sit down, and continue doing other things until called

Node is optimized for the second model.


Diagram Descriptions

1. Blocking Execution Timeline

Visualize:

  • A single horizontal line representing the main thread

  • Task A starts and occupies the entire line

  • Tasks B and C are queued behind it

  • No overlap, strictly sequential execution

Key idea:

  • One task at a time, everything else waits

2. Non-Blocking Execution Timeline

Visualize:

  • Main thread line continues moving forward

  • Task A starts, then is delegated outward to a worker

  • Main thread immediately starts Task B and Task C

  • Later, Task A completes and its callback is inserted back

Key idea:

  • Delegation allows overlap of waiting time with useful work

Final Takeaways

  • Blocking is not just slower code. It is a system-level bottleneck

  • Non-blocking is not magic parallelism. It is efficient scheduling

  • Async I O improves throughput, but:

    • CPU-heavy logic can still kill performance

    • Poor callback design can still block the loop

If you are building APIs, the real question is not sync vs async syntax.

It is:

Does this code keep the event loop free?

That is the difference between a server that scales and one that stalls.