Skip to main content

Command Palette

Search for a command to run...

How Node.js Handles Multiple Requests with a Single Thread

Published
6 min read
How Node.js Handles Multiple Requests with a Single Thread

One of the most confusing things about Node.js is that it runs on a single thread, yet it can handle thousands of concurrent requests. This seems like a contradiction. How does a single thread handle multiple requestsa without blocking? The answer lies in understanding concurrency versus parallelism and the event loop.

Single-Threaded: What It Means

Node.js runs your JavaScript code on a single thread. There's only one thread executing your code at any given moment.

Contrast this with languages like Java or Python, where you might create a new thread for each request. With Node.js, you can't do that. You get one thread, and that's it.

If you tried to handle requests synchronously and sequentially, your server would be painfully slow. The first request would block all others.

Concurrency vs Parallelism

This is critical to understand:

Parallelism means doing multiple things at the exact same time. You need multiple processors or cores.

Concurrency means managing multiple tasks without necessarily doing them at the same time. You interleave tasks.

Node.js achieves concurrency, not parallelism, through its single thread.

Think of a restaurant again. One waiter (single thread) can handle many customers (requests) concurrently by:

  1. Taking customer A's order

  2. Passing it to the kitchen (delegating work)

  3. Taking customer B's order

  4. Checking if customer A's food is ready

  5. Serving customer A

  6. Taking customer C's order

  7. Checking if customer B's food is ready

  8. Serving customer B

The waiter isn't cooking (just like Node.js doesn't perform database queries itself). The waiter is coordinating and serving, while the kitchen does the actual work.

The Event Loop and Worker Threads

When you perform slow operations like reading files or querying databases, Node.js doesn't have your thread wait. Instead, it delegates the work to background workers.

Here's what happens:

import fs from 'fs';

console.log('Starting');

fs.readFile('large-file.txt', 'utf8', (err, data) => {
  console.log('File read complete');
});

console.log('Continuing');

Execution flow:

  1. "Starting" is logged

  2. readFile is called, work is delegated to a background worker

  3. "Continuing" is logged immediately (no wait!)

  4. Background worker reads the file

  5. Once complete, the callback is queued

  6. Event loop picks up the callback and executes it

  7. "File read complete" is logged

Output:

Starting
Continuing
File read complete

Your thread never blocks. It quickly delegates work and moves on to handle other things.

How Node.js Handles Multiple Requests

Here's a more realistic server example:

import express from 'express';
import fs from 'fs';

const app = express();

app.get('/read-file', (req, res) => {
  fs.readFile('data.txt', 'utf8', (err, data) => {
    if (err) {
      res.status(500).send('Error reading file');
    } else {
      res.send(data);
    }
  });
});

app.get('/db-query', (req, res) => {
  // Simulating a database query that takes 1 second
  setTimeout(() => {
    res.send('Query complete');
  }, 1000);
});

app.listen(3000, () => console.log('Server running'));

Imagine two requests arrive at almost the same time:

  1. Request 1 comes in → Server starts reading a file → Work delegated to background thread

  2. Request 2 comes in → Server performs a database query → Work delegated to background thread

  3. Node.js thread is free and waiting for events

  4. File read completes → Callback executed → Response sent to client 1

  5. Database query completes → Callback executed → Response sent to client 2

Both operations happened concurrently, but your JavaScript code only ran on one thread. The thread quickly delegates work and moves on.

Event Loop Visualization

The event loop continuously checks if there's work to do:

Main Thread (JavaScript execution)
         |
    -----+-----
   |          |
Sync Code  Callbacks
   |          |
   |    Event Loop checks:
   |    1. Is there a completed operation?
   |    2. Execute its callback
   |    3. Go back to step 1

Here's a concrete example:

console.log('1: Start');

setTimeout(() => {
  console.log('2: Timer complete');
}, 100);

console.log('3: Still on main thread');

Output:

1: Start
3: Still on main thread
2: Timer complete

Even though the timer was set first, the synchronous code runs first. The callback waits until the main thread is free.

Why This Scales Well

This design is powerful because:

  1. No thread overhead: Creating a thread for each request is expensive. Node.js uses a single thread plus a pool of background workers.

  2. Efficient resource usage: A single thread uses far less memory than hundreds of threads.

  3. Avoids context switching: The operating system doesn't have to switch between many threads.

  4. Non-blocking by default: Slow operations don't block other requests.

A server might handle 10,000 concurrent connections with a single thread because most of that time is spent waiting for databases, files, or network responses—none of which block the thread.

The Blocking Problem

What happens if you ignore this and write blocking code?

import express from 'express';
import fs from 'fs';

const app = express();

app.get('/slow-sync', (req, res) => {
  // This blocks the entire server!
  const data = fs.readFileSync('large-file.txt', 'utf8');
  res.send(data);
});

app.listen(3000);

If a request to /slow-sync takes 2 seconds, every other request waits 2 seconds. The single thread is occupied and can't handle other requests.

With 100 simultaneous requests, they queue up and wait for the thread. This destroys performance.

Worker Threads for CPU-Intensive Work

For CPU-intensive operations (like complex calculations), Node.js has worker threads. You can offload heavy computations to prevent blocking:

import express from 'express';
import { Worker } from 'worker_threads';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.get('/cpu-intensive', (req, res) => {
  // Delegate to a worker thread
  const worker = new Worker(path.join(__dirname, 'worker.js'));
  
  worker.on('message', (result) => {
    res.json({ result });
  });
  
  worker.postMessage({ data: 'process this' });
});

app.listen(3000);

This keeps the main thread responsive while a worker does the heavy computation.

Key Takeaways

  • Node.js runs JavaScript on a single thread, achieving concurrency, not parallelism

  • Slow operations like I/O are delegated to background workers

  • While workers are busy, the main thread handles other requests

  • The event loop coordinates everything

  • This model scales well and uses resources efficiently

  • Blocking code (synchronous operations) kills performance

  • For CPU-intensive tasks, use worker threads

  • The single-threaded model is a feature, not a limitation

Understanding this is key to writing efficient Node.js applications. Don't block the thread, let it coordinate work, and your server will handle massive concurrent load.