Grokking Node.js
Vote

0% completed

​

The Event-Driven, Non-Blocking I/O Model

Waiting by Blocking

Waiting Without Blocking

The Event Loop Runs the Callbacks

A Web Server Uses the Same Model

The Cost: Calculation Blocks Everything

Key Takeaways

Three database queries that each take 100 ms can finish together in about 100 ms, or one after another in about 300 ms. The difference comes from what the program does while it waits. This lesson runs both versions and then explains the model that Node.js uses to get the faster result.

Waiting by Blocking

The examples below do not need a real database. A function that waits 100 ms before it answers behaves the same way for this purpose, so each example uses one as a stand-in for a slow query.

The first version waits the way many programs do: it does nothing else until the answer arrives. Code that stops the program until an operation finishes is called blocking.

// Stands in for a database query that takes 100 ms, written so that it blocks. function slowQuerySync(name) { const end = Date.now() + 100; while (Date.now() < end) {} // keeps the thread busy for 100 ms return `${name} loaded`; } const start = Date.now(); console.log(slowQuerySync('users'), 'after', Date.now() - start, 'ms'); console.log(slowQuerySync('orders'), 'after', Date.now() - start, 'ms'); console.log(slowQuerySync('products'), 'after', Date.now() - start, 'ms');
users loaded after 100 ms orders loaded after 201 ms products loaded after 301 ms

The second query cannot start until the first one has returned, and the third waits for both. The waits add up to 300 ms, although the processor did no useful work during any of them.

Waiting Without Blocking

The second version starts each query and gives it a function to call when the answer is ready. A function passed in this way is called a callback. The program does not wait for the answer; it moves straight to the next line.

// Stands in for the same query, written so that it does not block. function slowQuery(name, callback) { setTimeout(() => callback(`${name} loaded`), 100); } const start = Date.now(); slowQuery('users', (result) => console.log(result, 'after', Date.now() - start, 'ms')); slowQuery('orders', (result) => console.log(result, 'after', Date.now() - start, 'ms')); slowQuery('products', (result) => console.log(result, 'after', Date.now() - start, 'ms')); console.log('All three queries started after', Date.now() - start, 'ms');
All three queries started after 0 ms users loaded after 109 ms orders loaded after 109 ms products loaded after 109 ms

The events happen in this order. All three queries start within the same millisecond, and the last line of the script runs before any result has arrived. The three waits then overlap, and after about 100 ms all three callbacks run, one after another. The exact number varies by a few milliseconds from one run to the next.

Three 100 ms queries take 300 ms when each one blocks, and about 100 ms when their waits overlap.
Three 100 ms queries take 300 ms when each one blocks, and about 100 ms when their waits overlap.

In the diagram, compare the length of the two timelines. Neither version makes a single query faster. The non-blocking version is faster overall because it does not leave the thread idle while a query is waiting.

The Event Loop Runs the Callbacks

Input and output, usually shortened to I/O, means any operation that moves data between your program and something outside it: a file, a database, a network connection. Node.js performs I/O in the non-blocking way shown above. When your code reads a file, Node.js hands the reading to a helper outside your JavaScript and returns at once; it does not stop and wait for the data. The next lesson shows which part of Node.js does that work.

Each finished operation is an event, and the callback that you supplied is the code that responds to it. Node.js keeps a list of finished events, and a loop called the event loop takes them one at a time and runs their callbacks. Programs built around waiting for events and responding to them are called event-driven.

All of your JavaScript runs on one thread, the main thread. The event loop never runs two callbacks at the same moment. What happens at the same time is the waiting, and the waiting is done by Node.js and the operating system, not by your JavaScript.

A Web Server Uses the Same Model

A web server spends most of its time waiting. A request arrives, the server queries a database, and it waits for the answer before it can reply. Suppose 50 users each send a request whose database query takes 100 ms.

A blocking server with one thread would answer them one after another, and the last user would wait about 5 seconds. A server that creates one thread per request avoids that delay, but it pays for 50 threads, and each thread reserves memory and costs the operating system time to switch between them. Node.js starts all 50 queries, waits for all of them at once on its single thread, and answers each user when that user's query returns.

This is why Node.js suits programs that handle many connections and spend most of their time on I/O: APIs that read and write databases, chat systems, notification services and live dashboards.

The Cost: Calculation Blocks Everything

The single thread is also the limit of the model. A callback can only run when the main thread is free, so code that keeps the thread busy delays every other callback in the process.

const start = Date.now(); setTimeout(() => { console.log('Timer fired after', Date.now() - start, 'ms'); }, 10); // CPU-heavy work: count for about 500 ms. let total = 0; while (Date.now() - start < 500) { total += 1; } console.log('Loop finished after', Date.now() - start, 'ms');
Loop finished after 500 ms Timer fired after 501 ms

The timer was due after 10 ms, but it fired after 501 ms. Its time had passed, yet its callback could not run until the loop had released the thread. In a web server, the same loop would delay the reply to every user for half a second.

Use Node.js freely for work that mostly waits. For work that mostly calculates, such as resizing images, compressing large files or running long numeric jobs, plan to move the calculation off the main thread or into another process.

Key Takeaways

  • Blocking code waits for each operation to finish before it starts the next; non-blocking code starts the operation, supplies a callback, and continues.
  • Node.js performs I/O without blocking, so the waits for many operations overlap. Three 100 ms waits took about 100 ms instead of 300 ms.
  • The event loop runs callbacks one at a time on a single main thread, as their operations finish.
  • Long calculations block that thread, and every other callback waits until they end.

Reading Progress

0%


Vote for new content

On This Page

Waiting by Blocking

Waiting Without Blocking

The Event Loop Runs the Callbacks

A Web Server Uses the Same Model

The Cost: Calculation Blocks Everything

Key Takeaways