On this page

Blocking vs Non-Blocking I/O

Every time your code reads a file or talks to a database, it has to wait for the result. What your program does while it waits is the difference between blocking and non-blocking I/O.

Definition

  • Blocking I/O: the main thread stops and waits until the I/O finishes. Nothing else runs in the meantime.
  • Non-blocking I/O: the main thread starts the I/O, hands it to libuv, and moves on. A callback runs later, when the result is ready.

I/O (Input/Output) means any work outside your program: reading files, network requests, database queries.

Analogy: think of ordering coffee.

  • Blocking: you order and stand at the counter until your coffee is ready. The line behind you can't move.
  • Non-blocking: you order, get a token, and step aside. The next customer orders, and your name is called when the coffee is ready.
BlockingNon-Blocking
Main threadWaitsKeeps running
Result comes backAs the return valueIn a callback, promise, or await
Node.js examplefs.readFileSyncfs.readFile, fs.promises.readFile
Other requestsMust waitHandled in the meantime
Naming hintMethods ending in SyncMethods without Sync

Blocking Example

const fs = require("fs");
console.log("Start");
const data = fs.readFileSync("data.txt", "utf8"); // waits here
console.log("File read");
console.log("End");
// Output: Start, File read, End

The program stops at readFileSync until the whole file is read. End cannot print until then.

Non-Blocking Example

const fs = require("fs");
console.log("Start");
fs.readFile("data.txt", "utf8", (err, data) => {
console.log("File read");
});
console.log("End");
// Output: Start, End, File read

readFile hands the work to libuv (it runs on the thread pool), and the main thread moves on. File read prints later, when the event loop runs the callback.

Why It Matters on a Server

A Node.js server handles every request on one main thread. One blocking call makes every user wait.

const http = require("http");
const fs = require("fs");
// Blocking: each request freezes the server while the file is read
http
.createServer((req, res) => {
const data = fs.readFileSync("big-file.txt");
res.end(data);
})
.listen(3000);
// Non-blocking: other requests are handled while the file is read
http
.createServer((req, res) => {
fs.readFile("big-file.txt", (err, data) => {
res.end(data);
});
})
.listen(3001);

If reading the file takes 100 ms and 10 users arrive at once:

ServerWhat happens
BlockingRequests are served one after another, so the last user waits about 1 second
Non-blockingAll 10 reads start almost at once, and each user waits about 100 ms

Three Ways to Write Non-Blocking Code

All three are non-blocking. Only the style is different.

const fs = require("fs");
const fsp = require("fs/promises");
// 1. Callback
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// 2. Promise
fsp
.readFile("data.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
// 3. async / await (recommended)
async function read() {
try {
const data = await fsp.readFile("data.txt", "utf8");
console.log(data);
} catch (err) {
console.error(err);
}
}

Note: await looks like blocking code, but it only pauses that async function. The main thread is free to run other code.

Mistake: Mixing Blocking and Non-Blocking

const fs = require("fs");
// Wrong: the file may be deleted before it is read
fs.readFile("temp.txt", "utf8", (err, data) => {
console.log(data);
});
fs.unlinkSync("temp.txt"); // runs immediately
// Right: delete only after the read has finished
fs.readFile("temp.txt", "utf8", (err, data) => {
console.log(data);
fs.unlink("temp.txt", () => {});
});

Non-blocking code does not wait, so any step that depends on the result must go inside the callback (or after await).

When Is Blocking Code Okay?

OkayAvoid
Loading a config file at startup, before the server startsInside request handlers
Small CLI scripts with one userIn anything that serves many users
One-time setup tasksIn loops or frequently called functions

Summary

  • Blocking I/O makes the main thread wait. Non-blocking I/O lets it keep running.
  • In Node.js, methods ending in Sync are blocking.
  • Non-blocking I/O gives its result through a callback, promise, or await.
  • On a server, one blocking call delays every user, because there is only one main thread.
  • await pauses only its own async function, not the main thread.
  • Put code that depends on the result inside the callback or after await.
  • Blocking code is fine at startup and in simple scripts, but never in request handlers.

Interview Answer: What Is the Difference Between Blocking and Non-Blocking I/O?

Blocking I/O stops the main thread until the operation finishes, so nothing else can run in the meantime. In Node.js, methods ending in Sync, such as fs.readFileSync, are blocking.

Non-blocking I/O starts the operation, hands it to libuv, and lets the main thread continue. The result comes back later through a callback, a promise, or await, and the event loop runs it when the main thread is free.

Since Node.js runs JavaScript on a single thread, one blocking call in a request handler makes every user wait. So I use non-blocking APIs like fs.promises with async/await in servers, and I use sync methods only for one-time tasks like loading config at startup.

The next topic is worker threads.

Next TopicCommonJS vs ES Modules, Module Caching