🌱BeginnerNode.js Foundations

Single-Threaded Architecture

How Node.js runs JavaScript on one thread and why that is a feature, not a limitation

Overview

Node.js runs all JavaScript code on a single thread. If you are coming from languages like Java or C# where servers typically spawn a new thread per request, this sounds alarming. How can a single-threaded server handle thousands of concurrent connections without grinding to a halt? The answer lies in what "single-threaded" actually means in practice β€” and what Node delegates to other threads under the hood. The JavaScript engine (V8) executes your code on one thread, which means only one piece of JS runs at any given moment. There are no race conditions on shared JS objects, no need for mutexes or synchronized blocks, and no risk of two callbacks accidentally corrupting the same variable simultaneously. This constraint is what makes Node.js programs so much easier to reason about compared to multi-threaded servers. However, Node does not do everything on that one thread. Blocking I/O operations β€” reading a file, making a database query, sending an HTTP request β€” are handed off to the operating system or a thread pool managed by libuv, Node's underlying C++ library. When the OS finishes the work, it notifies Node, which queues a callback to run on the JS thread. Your JavaScript never blocks; it just registers a callback and moves on. This architecture is called the reactor pattern, and it makes Node extraordinarily efficient for I/O-heavy workloads like web servers, APIs, and real-time applications. Where it struggles is CPU-heavy work: if you run a tight loop for two seconds of computation, you block the entire JS thread and every other request waits. That is why Node provides Worker Threads and Child Processes for CPU-bound tasks β€” topics covered later in this course.

Key Points

  • β€’JavaScript executes on exactly one thread in Node.js β€” only one callback runs at a time
  • β€’Blocking I/O (files, network, DNS) is offloaded to libuv's thread pool or the OS kernel, not the JS thread
  • β€’No race conditions on shared JS state because concurrent code literally cannot run at the same time
  • β€’The single thread becomes a bottleneck only for CPU-intensive work, not for I/O-heavy workloads
  • β€’This model enables handling tens of thousands of concurrent connections with very low memory overhead
  • β€’Worker Threads and Child Processes exist specifically to move CPU work off the main thread

Examples

Demonstrating that JS is single-threaded β€” setTimeout does not interrupt running code

// Even though we schedule a timeout for 0ms,
// it cannot fire while synchronous code is running.
setTimeout(() => {
  console.log('timeout fired');
}, 0);

// This loop runs fully before the timeout callback can execute
for (let i = 0; i < 1_000_000; i++) { /* busy work */ }

console.log('sync done');
// Output order:
// sync done
// timeout fired

The event loop only picks up pending callbacks after the current synchronous execution finishes.

I/O does not block the thread β€” Node continues executing while waiting for the file

const fs = require('fs');

fs.readFile('data.json', 'utf8', (err, data) => {
  // This runs later, after the OS reads the file
  console.log('file read complete, length:', data.length);
});

// This runs immediately β€” we did not wait for the file
console.log('registered read callback, moving on...');

// Output:
// registered read callback, moving on...
// file read complete, length: 1234

Blocking the event loop β€” what NOT to do with synchronous CPU work

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/slow') {
    // BAD: this blocks all other requests for ~3 seconds
    const end = Date.now() + 3000;
    while (Date.now() < end) { /* spin */ }
    res.end('slow done');
  } else {
    res.end('fast');
  }
});

server.listen(3000);
// While /slow is processing, ANY /fast request also
// has to wait β€” the entire server is frozen.

Move CPU-heavy work to Worker Threads or a child process so the event loop stays free.

Common Mistakes

  • Assuming "single-threaded" means Node is slow β€” it actually outperforms thread-per-request servers for I/O workloads because it avoids context-switching overhead
  • Running synchronous CPU loops (sorting huge arrays, crypto without worker) on the main thread and wondering why the server becomes unresponsive
  • Thinking that async/await makes code truly parallel β€” it still runs on the one JS thread, just non-blocking
  • Forgetting that libuv's default thread pool size is 4; DNS lookups, fs operations, and some crypto can queue behind each other

Note

Run `node -e "console.log(process.versions)"` to see the exact V8 and libuv versions bundled with your Node install. Understanding which libuv version you have tells you the default UV_THREADPOOL_SIZE and what async operations it supports.

Exercises

Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.

Loading editor…

Completed: 0 / 6 exercises

Knowledge Check

Question 1 of 4

Which statement best describes Node.js's single-threaded model?