Skip to content

Express Performance Optimization

Node.js runs your JavaScript on a single thread, so understanding what blocks that thread, and how to scale across multiple CPU cores, is central to Express performance. This lesson covers the most impactful, practical techniques.

The Single-Threaded Event Loop

Express (like all of Node.js) processes requests on a single-threaded event loop. Fast, non-blocking operations (most I/O, database calls, file reads) don't hold up other requests, but any CPU-intensive synchronous code, heavy computation, large synchronous JSON parsing, blocks every other in-flight request until it finishes.

The two biggest performance levers are, therefore, avoiding blocking code on the hot path, and running multiple Node processes (via clustering or a process manager) to use all available CPU cores.

const compression = require('compression');
app.use(compression());

// avoid: blocks the event loop for the request's whole duration
app.get('/report', (req, res) => {
  const result = heavySynchronousComputation(); // BAD on the hot path
  res.json(result);
});

compression() reduces response payload size over the network; the synchronous computation example shows exactly the kind of code that blocks every other request.

Offloading CPU-Intensive Work

const { Worker } = require('worker_threads');

// or offload to a background job queue (BullMQ, etc.) for longer tasks
app.post('/generate-report', (req, res) => {
  queue.add('generate-report', req.body);
  res.status(202).json({ status: 'queued' });
});
  • Worker threads move CPU-heavy computation off the main event loop.
  • For long-running work, queue a background job and respond immediately with 202 Accepted.
  • Never run synchronous, CPU-heavy loops directly inside a route handler.
  • Prefer async, non-blocking I/O (database drivers, fs.promises) throughout your codebase.

Express Performance Cheatsheet

Common performance techniques and what they address.

Technique Addresses
compression() Large response payload size over the network
Clustering / PM2 Using only one CPU core out of many available
Worker threads / job queues CPU-intensive work blocking the event loop
Caching (see Caching lesson) Repeated, expensive computation or DB queries
Connection pooling Slow, repeated database connection setup
Pagination Unbounded response sizes from large collections

Clustering Across CPU Cores

A single Node.js process only uses one CPU core. Node's built-in cluster module (or a process manager like PM2 in cluster mode) forks multiple worker processes, each running its own copy of your Express app, sharing the same port and distributing incoming connections between them.

// cluster.js
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  const cpuCount = os.cpus().length;
  for (let i = 0; i < cpuCount; i++) cluster.fork();

  cluster.on('exit', () => cluster.fork()); // restart a crashed worker
} else {
  require('./server'); // each worker runs the actual Express app
}

Measuring Before Optimizing

Guessing at bottlenecks wastes effort. Use console.time()/console.timeEnd() for quick local checks, or a proper profiler/APM tool (like Node's built-in profiler or a service like Datadog/New Relic) to find the actual slow paths before optimizing them.

Common Mistakes

  • Running expensive synchronous computation directly inside a route handler, blocking every other in-flight request.
  • Running a single Node process in production without clustering, leaving most CPU cores idle.
  • Optimizing code without first measuring where the actual bottleneck is.
  • Skipping compression on large JSON or HTML responses.

Key Takeaways

  • Node's event loop is single-threaded, blocking synchronous code stalls every other request.
  • Clustering or a process manager lets your app use every available CPU core.
  • Offload CPU-heavy work to worker threads or background job queues instead of the request path.
  • Always measure before optimizing, to target real bottlenecks instead of guessing.

Pro Tip

Run node --prof (or a dedicated profiler) against a realistic load test before assuming you know where the bottleneck is, Express performance problems are very often somewhere unexpected, like an unindexed database query.