Performance Tuning

Compress responses, cache aggressively, keep the event loop free, and measure before you optimize.

Express itself is rarely your bottleneck — your handlers are. Still, a handful of framework-level habits give real wins, and one rule governs all of them: measure first. Profile, find the actual hot path, then optimize that.

High-leverage wins

  • Compression — gzip/brotli responses. Best done at your reverse proxy (Nginx) so Node spends its cycles on your logic, not on compressing bytes.
  • HTTP caching — set Cache-Control and ETag on cacheable responses so clients and CDNs skip the round trip entirely. The cheapest request is the one you never receive.
  • Never block the event loop — one synchronous JSON.parse of a huge payload, a sync crypto call, or a tight loop stalls every concurrent request. Offload CPU-heavy work to a worker thread.
  • Reuse connections — one shared DB pool, one keep-alive HTTP agent for outbound calls.

Set NODE_ENV=production

Express caches view lookups and skips verbose error work when NODE_ENV=production. Forgetting this flag alone can cost a measurable chunk of throughput.

Example

Example · javascript
import express from 'express';
import { Worker } from 'node:worker_threads';

const app = express();

// 1. HTTP caching: let clients and CDNs skip the round trip.
app.get('/api/catalog', (req, res) => {
  const catalog = getCatalog();
  res.set('Cache-Control', 'public, max-age=300'); // 5 min
  res.json(catalog); // res.json sets a weak ETag automatically
});

// 2. Keep the event loop free: offload CPU-bound work to a worker thread.
app.get('/report/:id', (req, res, next) => {
  const worker = new Worker('./workers/report.js', {
    workerData: { id: req.params.id },
  });
  worker.once('message', (data) => res.json(data));
  worker.once('error', next); // forwarded to the error handler
});

// 3. Compression is usually better at the proxy, but if you must do it in
//    Node, do it conditionally — never compress already-compressed assets.
// import compression from 'compression';
// app.use(compression({ threshold: 1024 }));

// Remember: NODE_ENV=production unlocks Express's own fast paths.

When to use it

  • A developer adds compression middleware to an Express API so JSON responses are gzip-compressed, reducing payload sizes by up to 70%.
  • A team caches expensive database query results in Redis so repeated GET requests return in under 1 ms instead of querying the database every time.
  • A developer profiles an Express route with clinic.js to find a slow middleware and replaces it with a lightweight alternative.

More examples

Response compression

Registers compression with a size threshold and an override header so callers can opt out of compression for streaming endpoints.

Example · js
const compression = require('compression');
const express = require('express');
const app = express();

app.use(compression({
  level: 6,                         // zlib level 1-9
  threshold: 1024,                  // only compress responses > 1 KB
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
}));

app.get('/data', (req, res) => res.json(largeArray));
app.listen(3000);

Redis response cache middleware

A cache middleware checks Redis for a stored response; on a hit it returns immediately, on a miss it intercepts res.json to store the result.

Example · js
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
client.connect();

function cache(ttlSeconds) {
  return async (req, res, next) => {
    const key = `cache:${req.url}`;
    const hit = await client.get(key);
    if (hit) return res.json(JSON.parse(hit));
    res.sendJson = res.json.bind(res);
    res.json = (body) => {
      client.setEx(key, ttlSeconds, JSON.stringify(body));
      res.sendJson(body);
    };
    next();
  };
}

app.get('/products', cache(60), ctrl.listProducts);

Keep-Alive and connection pooling

Tunes keepAliveTimeout above the AWS ALB idle timeout to prevent the load balancer from closing connections that Express still considers open.

Example · js
const http = require('http');
const app = require('./app');

const server = http.createServer(app);
server.keepAliveTimeout = 65_000;  // longer than ALB idle timeout
server.headersTimeout   = 66_000;

server.listen(process.env.PORT || 3000, () => {
  console.log('Server with keep-alive tuning started');
});

Discussion

  • Be the first to comment on this lesson.