Expressjs
Multithreading
Nodejs
Web Development
Concurrency

Expressjs multiple threads

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Express applications run on Node.js, which is often described as single-threaded. That description is only partly true. JavaScript request handling usually runs on one main event-loop thread per process, but Node can still use multiple CPU cores and background threads through worker processes, worker threads, and the libuv thread pool.

What "Single-Threaded" Really Means in Express

An ordinary Express server handles incoming requests on one JavaScript thread in one process. That is why blocking CPU-heavy work is dangerous: if one request spends a long time doing computation, the event loop cannot serve other requests efficiently.

A simple Express server looks like this:

javascript
1const express = require("express");
2
3const app = express();
4
5app.get("/", (req, res) => {
6  res.send("hello");
7});
8
9app.listen(3000, () => {
10  console.log("listening on port 3000");
11});

This server can handle many concurrent requests because I/O is asynchronous, not because Express creates a thread per request.

That distinction matters. Concurrency in Node is often event-loop concurrency, not thread-per-request concurrency like some other platforms.

Use Multiple Processes for More HTTP Throughput

If you want an Express app to use multiple CPU cores for serving requests, the classic approach is to run multiple Node processes. Node’s cluster module makes that easy:

javascript
1const cluster = require("cluster");
2const os = require("os");
3const express = require("express");
4
5if (cluster.isPrimary) {
6  const workers = os.cpus().length;
7  for (let i = 0; i < workers; i++) {
8    cluster.fork();
9  }
10} else {
11  const app = express();
12
13  app.get("/", (req, res) => {
14    res.send(`handled by process ${process.pid}`);
15  });
16
17  app.listen(3000);
18}

Now several processes can accept requests on the same port, which lets the operating system distribute load across CPU cores.

This is not multithreading inside one Express request handler. It is multi-process scaling, which is often the right answer for web throughput.

Use Worker Threads for CPU-Bound Tasks

If the problem is not HTTP concurrency but CPU-heavy work inside a request path, worker threads are often a better tool than clustering. They let you offload computation while keeping the main event loop responsive.

Example worker:

javascript
1const { Worker, isMainThread, parentPort, workerData } = require("worker_threads");
2
3function expensiveCalculation(limit) {
4  let total = 0;
5  for (let i = 0; i < limit; i++) {
6    total += i;
7  }
8  return total;
9}
10
11if (!isMainThread) {
12  parentPort.postMessage(expensiveCalculation(workerData.limit));
13}

Used from Express:

javascript
1const express = require("express");
2const { Worker } = require("worker_threads");
3
4const app = express();
5
6app.get("/compute", (req, res) => {
7  const worker = new Worker("./worker.js", {
8    workerData: { limit: 50_000_000 }
9  });
10
11  worker.once("message", result => res.json({ result }));
12  worker.once("error", err => res.status(500).json({ error: err.message }));
13});
14
15app.listen(3000);

This is the pattern to use when one endpoint must perform real computation without freezing the main server thread.

Do Not Forget the libuv Thread Pool

Node already uses a background thread pool for some built-in operations such as certain filesystem and crypto work. That means Node is not literally one thread doing everything. But that pool does not magically parallelize your JavaScript route handlers.

So there are three different ideas to keep straight:

  • the Express request handler runs on the main event loop
  • libuv uses background threads for selected native operations
  • worker processes and worker threads are explicit tools for additional parallelism

Confusing these layers is the source of many "is Express multithreaded" misunderstandings.

Common Pitfalls

The biggest mistake is assuming Express automatically becomes multithreaded under load. It does not. One process still has one main JavaScript event loop.

Another issue is using worker threads for every tiny task. Offloading has overhead, so it is worth it mainly for real CPU-bound work, not trivial logic.

Developers also sometimes scale HTTP throughput with worker threads when what they really want is multiple server processes across CPU cores. For serving more requests, clustering or a process manager is usually the more natural tool.

Finally, even in a multi-process setup, shared mutable in-memory state becomes harder. Session storage, caches, and counters often need an external system such as Redis if several workers must see the same data.

Summary

  • Express route handlers normally run on one main JavaScript thread per Node process.
  • Asynchronous I/O gives concurrency, but not thread-per-request execution.
  • Use multiple processes, often through cluster or a process manager, to scale request serving across CPU cores.
  • Use worker threads for CPU-heavy work that would otherwise block the event loop.
  • Keep event-loop concurrency, background thread pools, and explicit parallelism as separate concepts.

Course illustration
Course illustration

All Rights Reserved.