Please Don’t Block the Event Loop: Understanding Sync vs Async Code in Node.js
Why your server is stalling, how Node handles concurrency, and what it means to write truly non-blocking code.
Search for a command to run...
Why your server is stalling, how Node handles concurrency, and what it means to write truly non-blocking code.
No comments yet. Be the first to comment.
These are the assignments or knowledge dumps I decide to write for the "Chai aur Code's web Dev Cohort 2026"
Unpacking the event loop, background workers, and why doing one thing at a time is the secret to scaling.
RAG helps LLMs answer questions using your own data, but it doesn't guarantee correct answers. Here's how the pipeline works and where it commonly breaks down.

Imagine asking an AI, "Write a blog," versus "Act as an SEO expert and write a 1,000-word blog on AI safety formatted in markdown." The difference in the resulting output is night and day. A prompt is

Spotify and Apple Music make it incredibly easy to showcase your live listening habits on your website. YouTube Music? Not so much. After doing some setup you can do an API call and fetch your account

Arthur C. Clarke's famous third law: "Any sufficiently advanced technology is indistinguishable from magic" perfectly captures how the pace of human innovation works. From carrying supercomputers in o

Why HTTP is amnesiac, the difference between transport and state, and how to choose the right auth strategy for your Node.js app.
You’ve built an API. You test it locally, and it’s snappy. You deploy it, a few users jump on, and everything feels great. Then, one user triggers a massive file export or a complex database aggregation, and suddenly, the entire application freezes. Other users can’t even load the homepage.
Congratulations, you’ve just blocked the event loop.
When I first started writing Node.js, this was the hardest mental shift to make. Coming from multi-threaded languages, I was used to the idea that a heavy request only ruins the day for the thread handling it. But Node.js is fundamentally different. It is single-threaded by design. That single thread is your only waiter in a very busy restaurant. If you ask that waiter to go cook a five-course meal, the front of the house grinds to a halt.
Understanding the difference between blocking and non-blocking code isn’t just a theoretical exercise for an interview—it is the core mechanic of writing Node.js applications that scale.
Let’s break down what these terms actually mean, how they impact your server, and how to write code that keeps the engine running.
To understand Node’s architecture, think of a coffee shop with a single barista at the register.
The Blocking Model: You order a complex pour-over coffee. The barista takes your order, turns around, boils the water, grinds the beans, pours the coffee, and hands it to you. Then they take the next customer’s order. If there are ten people in line, the tenth person is going to be waiting a very long time, doing absolutely nothing, just because you wanted a fancy coffee.
The Non-Blocking Model: You order a pour-over. The barista takes your order, writes it on a cup, and hands it to a dedicated brewer in the back. They hand you a pager (a promise that you’ll get your coffee later) and immediately turn to the next person in line to take their order.
Node.js is that second barista. The single thread takes requests, delegates the heavy, slow work (I/O) to the background, and keeps moving.
In Node.js, blocking refers to operations that must complete before the JavaScript engine can move on to the next line of code. Because JavaScript executes synchronously by default on a single thread, any operation that takes time—like reading a large file, performing heavy cryptography, or running a massive for loop—will block the execution of everything else.
Here is what blocking looks like in the real world:
const fs = require('fs');
console.log("1. Starting to read file...");
// ⚠️ BLOCKING OPERATION
const data = fs.readFileSync('/large-data.json', 'utf8');
console.log("2. File read complete. Length:", data.length);
console.log("3. Moving to the next task.");
Why this slows servers: If this code sits inside an Express route, the server will stop processing all other incoming network requests until readFileSync is finished. Throughput drops to zero. Your server is entirely paralyzed, waiting on the hard drive to spin up and return data.
Non-blocking code allows the JavaScript execution thread to continue running while an operation happens in the background.
Instead of waiting for the file system or database to respond, Node.js offloads this work to the underlying system (specifically, a C++ library called libuv which handles system-level threads). Node just says, "Hey, start doing this. Call me back when you're done."
Here is the same file-reading scenario, written non-blockingly using Node’s async/await and Promises:
const fs = require('fs').promises;
console.log("1. Starting to read file...");
// ✅ NON-BLOCKING OPERATION
async function handleFile() {
// We tell Node to start reading, but we don't block the main thread.
const data = await fs.readFile('/large-data.json', 'utf8');
console.log("2. File read complete. Length:", data.length);
}
handleFile();
console.log("3. Moving to the next task.");
The Output Order:
Starting to read file...
Moving to the next task.
File read complete. Length: 150000
Notice the order. The main thread logged "1", kicked off the file read, and immediately moved on to log "3". It didn't wait. Later, when the file was completely read, Node pushed the callback back onto the event loop, and "2" was logged.
During the time the file was being read, your server could have handled thousands of other HTTP requests.
File handling is a classic example, but in modern web development, the most common asynchronous operation is talking to a database.
Network I/O is inherently slow compared to CPU speed. When you query a database, your code is sending a packet across a network, waiting for the database engine to parse the query, fetch data from a disk, serialize it, and send it back.
If Node didn't treat network requests as non-blocking, web servers would be unusable.
app.get('/users/:id', async (req, res) => {
try {
// The thread does NOT stop here.
// It pauses this specific function's execution,
// goes to handle other requests, and returns when the DB responds.
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
res.json(user);
} catch (error) {
res.status(500).send("Database error");
}
});
Because of async/await, the code reads top-to-bottom like synchronous, blocking code, but it behaves asynchronously. The Node event loop remains free to serve other users while the database does the heavy lifting.
The difference between blocking and non-blocking architecture is the difference between a server that scales and a server that crashes under moderate load.
If you have a blocking function that takes 100 milliseconds to execute, your server can theoretically only handle 10 requests per second.
If you make that same 100ms function non-blocking, your server's capacity is constrained only by memory and how many concurrent network sockets it can open. You can handle thousands of requests per second, because the thread is only spending fractions of a millisecond initiating the request and sending the response.
The golden rule of Node.js is simple: Don't block the Event Loop.
Never use *Sync methods (like readFileSync) in production web servers. Save them for CLI tools or initialization scripts that run before the server starts listening.
Delegate I/O (files, networks, databases) to asynchronous methods.
If you have heavy CPU-bound work (like image processing or complex math), don't run it on the main thread either. Offload it using Node's Worker Threads or a separate microservice.
Node.js is incredibly fast and highly concurrent, but it demands that you play by its rules. Give the thread back as quickly as possible, and let the system do the rest.