# Blocking vs Non-Blocking Code in Node.js

The first time I saw a Node.js server freeze completely because of one slow synchronous file read, I thought there was a bug in the framework. Surely a modern server runtime would not let one request block every other incoming connection? But Node.js absolutely will. And the reason is simple: if you write blocking code, Node.js has no choice but to block.

Understanding the difference between blocking and non-blocking code is not an advanced topic. It is foundational. Miss this and you will write servers that mysteriously slow to a crawl under load, and the problem will be entirely your fault, not Node.js.

* * *

## What Blocking Actually Means

Blocking code is code that stops the JavaScript thread from doing anything else until it finishes. The thread sits there, waiting, locked, unable to process any other work.

```javascript
const fs = require("fs");

console.log("Reading file...");
const data = fs.readFileSync("large-file.txt", "utf8"); // blocking
console.log("File read complete");
console.log(data.length);
```

`readFileSync` is a synchronous file read. The moment that line runs, the JavaScript thread stops. It waits for the entire file to be read from disk before moving to the next line. If that file is 500MB, the thread is frozen until all 500MB are loaded into memory. During that time, no other code runs. No other requests are processed. The server is completely blocked.

This is not a hypothetical. This is what actually happens.

* * *

## What Non-Blocking Looks Like

Non-blocking code starts an operation and immediately moves on. The operation runs in the background. When it finishes, a callback is invoked with the result.

```js
const fs = require("fs");

console.log("Reading file...");

fs.readFile("large-file.txt", "utf8", (err, data) => {
  if (err) {
    console.log("Error:", err);
    return;
  }
  console.log("File read complete");
  console.log(data.length);
});

console.log("This runs immediately, before the file is done reading");
```

`readFile` (without "Sync") is asynchronous. The file read starts, but the function returns immediately. The thread moves on. The callback is only called when the read finishes. That could be 10 milliseconds later or 500 milliseconds later. Either way, the thread is free to process other work while the file loads.

Output:

```plaintext
Reading file...
This runs immediately, before the file is done reading
File read complete
1048576
```

The second `console.log` runs before the file finishes reading. The JavaScript thread was not blocked.

![](https://cdn.hashnode.com/uploads/covers/69517a64b415eddac99049ec/1a400d57-0861-4f73-96c2-b1bf0922e9c0.png align="center")

* * *

## The Performance Difference

The impact of this difference becomes obvious the moment you have more than one user.

Imagine a server with a synchronous file read on every request:

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

http.createServer((req, res) => {
  const data = fs.readFileSync("report.json"); // blocks for ~200ms
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(data);
}).listen(3000);
```

Each request takes 200ms to read the file. During those 200ms, the server cannot process any other request. If 10 requests arrive simultaneously, they get processed one at a time. The 10th request waits 2 full seconds before it even starts.

Now the async version:

```js
http.createServer((req, res) => {
  fs.readFile("report.json", (err, data) => {
    if (err) {
      res.writeHead(500);
      res.end("Error reading file");
      return;
    }
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(data);
  });
}).listen(3000);
```

If 10 requests arrive simultaneously, all 10 file reads start at the same time. The operating system handles them in parallel. Most requests finish around 200ms, not 2 seconds. The throughput difference is dramatic.

This is why Node.js documentation constantly pushes async. The single-threaded event loop only works if you do not block it.

* * *

## When Blocking Code Is Actually Fine

There is one context where blocking code is acceptable: startup operations that run once before the server starts handling requests.

```js
const fs     = require("fs");
const config = JSON.parse(fs.readFileSync("config.json", "utf8"));

const server = http.createServer((req, res) => {
  // config is already in memory, no blocking here
  res.end(`Server running on port ${config.port}`);
});

server.listen(config.port);
```

The `readFileSync` runs once during server initialization. No requests are being handled yet. Blocking the thread here does not matter because there is nothing else the thread needs to do at that moment. Once the server starts listening, all file reads should be async.

The mental model: blocking is fine when you genuinely want everything to stop and wait. It is catastrophic when you want the server to remain responsive to other work.

* * *

## Common Blocking Operations and Their Async Equivalents

Here are the operations that developers most often use in blocking form without realizing the cost:

### File System

```js
// Blocking
const data = fs.readFileSync("data.txt");
fs.writeFileSync("output.txt", data);

// Non-blocking
fs.readFile("data.txt", (err, data) => {
  if (err) throw err;
  fs.writeFile("output.txt", data, (err) => {
    if (err) throw err;
    console.log("Write complete");
  });
});
```

### JSON Parsing Large Payloads

This one surprises people. `JSON.parse()` is synchronous and CPU-bound. If you parse a 10MB JSON response on the event loop, the thread is blocked for the entire parse duration.

```js
// Blocking
const huge = JSON.parse(largeJsonString); // locks thread

// Non-blocking alternative: use a worker thread for large payloads
const { Worker } = require("worker_threads");

const worker = new Worker(`
  const { parentPort, workerData } = require("worker_threads");
  const parsed = JSON.parse(workerData);
  parentPort.postMessage(parsed);
`, { eval: true, workerData: largeJsonString });

worker.on("message", parsed => {
  console.log("Parse complete without blocking event loop");
});
```

### Crypto Operations

```js
// Blocking
const crypto = require("crypto");
const hash   = crypto.pbkdf2Sync("password", "salt", 100000, 64, "sha512");

// Non-blocking
crypto.pbkdf2("password", "salt", 100000, 64, "sha512", (err, hash) => {
  if (err) throw err;
  console.log(hash.toString("hex"));
});
```

The `Sync` suffix is a universal signal in Node.js that an operation is blocking. If you see it in production request handlers, that is a red flag.

* * *

## A Real Mistake I Made

Early on I wrote a small API that processed uploaded CSV files. The endpoint received the file, parsed it, validated rows, and inserted them into a database.

```js
app.post("/upload", (req, res) => {
  const fileContent = fs.readFileSync(req.file.path); // blocking
  const rows        = parseCSV(fileContent);          // blocking, CPU-heavy

  rows.forEach(row => {
    db.insertSync(row); // blocking database call
  });

  res.json({ success: true, count: rows.length });
});
```

Under light testing with one user, it worked fine. Then it went into production and the server stopped responding whenever someone uploaded a large file. The file read blocked. The CSV parsing blocked. Every database insert blocked. For 5 seconds or more, the entire server froze. Other unrelated API calls timed out.

The fix was switching everything to async and using streams to process the CSV in chunks instead of loading it entirely into memory:

```js
const csv = require("csv-parser");

app.post("/upload", (req, res) => {
  let count = 0;

  fs.createReadStream(req.file.path)
    .pipe(csv())
    .on("data", async (row) => {
      await db.insert(row); // async insert
      count++;
    })
    .on("end", () => {
      res.json({ success: true, count });
    })
    .on("error", (err) => {
      res.status(500).json({ error: err.message });
    });
});
```

The stream processes rows one at a time without loading the entire file. The database inserts are async so the event loop stays free. The server remains responsive to other requests even during a large upload.

![](https://cdn.hashnode.com/uploads/covers/69517a64b415eddac99049ec/f276b985-c56a-47e9-a4db-aa3e0f11d787.png align="center")

This is what non-blocking actually looks like in production code.

* * *

## How to Spot Blocking Code

The most reliable signal is the `Sync` suffix in the Node.js standard library. If you see `readFileSync`, `writeFileSync`, `readdirSync`, `existsSync`, or any other `Sync` method inside a request handler, that is blocking code.

Beyond that, any CPU-intensive operation blocks by default: complex calculations, image processing, video encoding, large JSON parsing. Node.js cannot magically offload computation. If your code is actively running on the event loop, the event loop is blocked.

The solution for CPU work is either to break it into smaller chunks with `setImmediate` to give the event loop breathing room, or to use worker threads to offload the computation entirely.

* * *

## Async/Await and Blocking

One thing that trips people up: `async/await` does not make blocking code non-blocking. It just makes async code look synchronous.

```js
async function loadData() {
  const data = await fs.promises.readFile("data.txt"); // non-blocking
  return data;
}
```

The `await` pauses the function, but it does not block the event loop. Other callbacks can still run. This is non-blocking code written with clean syntax.

But:

```js
async function loadData() {
  const data = fs.readFileSync("data.txt"); // still blocking
  return data;
}
```

The `async` keyword does not fix blocking code. `readFileSync` blocks the thread regardless of how the function is defined. `async` is only syntactic sugar around Promises. It does not change the blocking behavior of synchronous operations.

* * *

## Quick Recap

Blocking code stops the JavaScript thread and prevents all other work from running until it finishes. Non-blocking code starts an operation, immediately returns, and calls a callback when the operation completes. In Node.js, blocking code during request handling kills scalability because the single-threaded event loop cannot process anything else while blocked. The `Sync` suffix in Node.js standard library methods is a clear signal that an operation is blocking. CPU-intensive operations are blocking by default and should be either chunked with `setImmediate` or offloaded to worker threads. `async/await` makes async code more readable but does not make blocking operations non-blocking. The choice between blocking and non-blocking is not about syntax style, it is about whether your server can handle more than one thing at a time.
