¿Qué diferencia hay entre I/O bloqueante y no bloqueante en Node.js, y por qué en un servidor nunca debes bloquear el event loop?
Ver respuesta — intenta responderla en voz alta primero
Una operación bloqueante (sync) detiene el único hilo de Node hasta que termina: nada más puede ejecutarse mientras tanto. Una operación no bloqueante (async) delega el trabajo lento (leer archivo, red, base de datos) al sistema y libera el hilo para atender otras cosas; cuando termina, avisa mediante un callback o una promesa. En un servidor jamás debes bloquear el event loop porque bloquearías a todos los usuarios a la vez.
I/O significa Input/Output: leer/escribir archivos, hacer consultas a una base de datos, llamadas de red. Son operaciones lentas comparadas con la velocidad de la CPU.
Bloqueante (síncrono): el hilo se queda esperando parado hasta que la operación termina. Si leer un archivo tarda 200 ms, tu único hilo estuvo 200 ms sin poder hacer absolutamente nada más.
No bloqueante (asíncrono): Node le pide al sistema "ve leyendo este archivo y avísame cuando termines", y sigue de largo ejecutando el resto del código. Cuando el archivo está listo, el callback (o la promesa) se pone en cola y el event loop lo ejecuta. El hilo nunca estuvo parado; estuvo atendiendo otras peticiones.
La regla de oro: en un servidor nunca bloquees el event loop. Como Node atiende a todos los usuarios con un solo hilo, si ese hilo se queda bloqueado esperando (o calculando), todos los demás usuarios quedan congelados hasta que se libere. Un solo readFileSync grande en la ruta equivocada puede tumbar el rendimiento de todo el servidor.
En la práctica esto significa:
- Usar siempre las versiones asíncronas de las funciones (
fs.readFile, nofs.readFileSync) dentro del servidor. - Preferir
async/awaitcon promesas para que el código asíncrono se lea fácil. - Reservar las funciones
...Syncsolo para código que corre una vez al arranque (por ejemplo, leer un archivo de configuración antes de que el servidor empiece a atender peticiones).
const fs = require("fs");
// --- BLOQUEANTE (sync): NO usar dentro de un servidor ---
// El hilo se detiene aqui hasta que el archivo termine de leerse.
const datos = fs.readFileSync("archivo.txt", "utf8");
console.log("Contenido:", datos);
console.log("Esto se imprime DESPUES de leer el archivo");
// --- NO BLOQUEANTE (async con callback) ---
fs.readFile("archivo.txt", "utf8", (err, datos) => {
if (err) return console.error("Error al leer:", err);
console.log("Contenido:", datos); // se imprime cuando el archivo esta listo
});
console.log("Esto se imprime ANTES de que el archivo termine de leerse");
// --- NO BLOQUEANTE (async con promesas + 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();
En la versión asíncrona, el console.log que dice "ANTES" se imprime primero, porque fs.readFile no bloquea: Node sigue ejecutando y solo vuelve al callback cuando el archivo está listo.
Usar funciones ...Sync (como fs.readFileSync, crypto.pbkdf2Sync, o un JSON.parse sobre un archivo enorme) dentro de un manejador de rutas de un servidor. Aunque tu código "funcione" en pruebas con un solo usuario, en producción con muchos usuarios simultáneos ese bloqueo congela el event loop y hace que la latencia se dispare para todos. Las versiones Sync solo son aceptables en scripts de una sola pasada o en el arranque de la aplicación.
"La diferencia es que una operación bloqueante detiene el único hilo de Node hasta que termina, mientras que una no bloqueante delega el trabajo lento al sistema mediante libuv y libera el hilo para seguir atendiendo. Por ejemplo, fs.readFileSync bloquea, y fs.readFile o su versión con promesas no. En un servidor nunca bloqueo el event loop porque, al ser un solo hilo el que atiende a todos, un bloqueo congela a todos los usuarios a la vez. Por eso uso siempre las versiones asíncronas dentro de las rutas y dejo las Sync solo para el arranque o scripts de una sola ejecución."
¿Qué imprime este código y por qué?
const fs = require("fs");
console.log("inicio");
fs.readFile("config.json", "utf8", () => {
console.log("archivo leido");
});
console.log("fin");
Ver respuesta
Imprime:
inicio
fin
archivo leido
fs.readFile es no bloqueante: Node registra el callback y sigue de largo, por eso fin se imprime antes que archivo leido. El callback solo se ejecuta cuando la lectura termina y el event loop lo procesa, lo cual ocurre después de que todo el código síncrono (incluido fin) ya corrió.