← Node & Express

Node Fundamentals

What is the difference between blocking and non-blocking I/O in Node.js, and why should you never block the event loop in a server?

Show answer — try answering out loud first

A blocking (sync) operation halts Node's single thread until it finishes: nothing else can run in the meantime. A non-blocking (async) operation delegates the slow work (reading a file, network, database) to the system and frees the thread to handle other things; when it finishes, it notifies you through a callback or a promise. In a server you must never block the event loop because you would block all users at once.

I/O means Input/Output: reading/writing files, querying a database, network calls. These are slow operations compared to CPU speed.

Blocking (synchronous): the thread stays idle waiting until the operation finishes. If reading a file takes 200 ms, your single thread spent 200 ms unable to do absolutely anything else.

Non-blocking (asynchronous): Node asks the system "start reading this file and let me know when you're done", and moves right along executing the rest of the code. When the file is ready, the callback (or the promise) is queued and the event loop runs it. The thread was never idle; it was handling other requests.

The golden rule: in a server, never block the event loop. Since Node serves all users with a single thread, if that thread gets blocked waiting (or computing), all other users are frozen until it is freed. A single large readFileSync in the wrong route can crater the performance of the entire server.

In practice this means:

  • Always use the asynchronous versions of functions (fs.readFile, not fs.readFileSync) inside the server.
  • Prefer async/await with promises so the asynchronous code reads easily.
  • Reserve the ...Sync functions only for code that runs once at startup (for example, reading a configuration file before the server begins handling requests).
const fs = require("fs");

// --- BLOCKING (sync): do NOT use inside a server ---
// The thread stops here until the file finishes being read.
const datos = fs.readFileSync("archivo.txt", "utf8");
console.log("Contenido:", datos);
console.log("Esto se imprime DESPUES de leer el archivo");

// --- NON-BLOCKING (async with callback) ---
fs.readFile("archivo.txt", "utf8", (err, datos) => {
  if (err) return console.error("Error al leer:", err);
  console.log("Contenido:", datos); // prints when the file is ready
});
console.log("Esto se imprime ANTES de que el archivo termine de leerse");

// --- NON-BLOCKING (async with promises + async/await) ---
const fsPromises = require("fs/promises");

async function leerArchivo() {
  try {
    const datos = await fsPromises.readFile("archivo.txt", "utf8");
    console.log("Contenido:", datos);
  } catch (err) {
    console.error("Error al leer:", err);
  }
}
leerArchivo();

In the asynchronous version, the console.log that says "ANTES" prints first, because fs.readFile does not block: Node keeps executing and only returns to the callback when the file is ready.

Using ...Sync functions (like fs.readFileSync, crypto.pbkdf2Sync, or a JSON.parse over a huge file) inside a server's route handler. Even though your code "works" in tests with a single user, in production with many simultaneous users that blocking freezes the event loop and makes latency spike for everyone. The Sync versions are only acceptable in one-shot scripts or during the application's startup.

"The difference is that a blocking operation halts Node's single thread until it finishes, while a non-blocking one delegates the slow work to the system through libuv and frees the thread to keep serving. For example, fs.readFileSync blocks, and fs.readFile or its promise-based version does not. In a server I never block the event loop because, since a single thread serves everyone, a block freezes all users at once. That's why I always use the asynchronous versions inside routes and leave the Sync ones only for startup or one-shot scripts."

Quick challenge

What does this code print and why?

const fs = require("fs");

console.log("inicio");
fs.readFile("config.json", "utf8", () => {
  console.log("archivo leido");
});
console.log("fin");
See answer

It prints:

inicio
fin
archivo leido

fs.readFile is non-blocking: Node registers the callback and moves right along, which is why fin prints before archivo leido. The callback only runs when the read finishes and the event loop processes it, which happens after all the synchronous code (including fin) has already run.