0x55aa
← Back to Blog

Node.js Compression: Stop Sending Your Users Bloated Responses 📦

5 min read

Here's a fun game: open your browser DevTools, go to the Network tab, and look at how big your API responses are. If you're not seeing a Content-Encoding: gzip or Content-Encoding: br header, congratulations — you're paying your cloud provider to send full, uncompressed JSON to every single client, every single request.

That's like mailing someone a novel by shipping every individual page in a separate envelope. Technically correct. Wildly wasteful.

Let's fix that.

What Is Response Compression, Anyway?

When your server sends a response, it's just bytes over the wire. JSON is human-readable text, and human-readable text is notoriously repetitive. Think about how many times the string "userId" appears in a paginated list response. Compression algorithms love repetition — they replace repeated patterns with tiny references, often shrinking JSON payloads by 60–80%.

There are two main algorithms you'll care about:

  • gzip — The old reliable. Supported everywhere, great compression, fast.
  • Brotli (br) — Google's newer algorithm. Better compression than gzip (often 15–20% smaller), but slightly slower to compress. All modern browsers support it.

The client tells your server what it supports via the Accept-Encoding request header. Your server picks the best option and sets Content-Encoding in the response. The client decompresses transparently. You pay less bandwidth. Everyone wins.

The Three-Line Fix for Express

If you're running Express, there's no excuse. The compression middleware handles everything automatically:

npm install compression
const express = require('express');
const compression = require('compression');

const app = express();

// Add this BEFORE your routes
app.use(compression());

app.get('/api/users', async (req, res) => {
  const users = await db.getUsers(); // Could be thousands of records
  res.json(users);                   // Now automatically compressed
});

app.listen(3000);

That's it. The middleware inspects the incoming Accept-Encoding header, compresses the response if the client supports it, and sets the right headers. Your routes don't change at all.

By default, the compression middleware uses gzip. For most production apps, that's perfectly fine.

Going Further: Brotli + Smarter Thresholds

The default compression middleware doesn't support Brotli out of the box (it's a Node.js core feature, not a separate library). Here's a slightly more production-grade setup that serves Brotli to modern clients and falls back to gzip for everything else:

const express = require('express');
const compression = require('compression');
const zlib = require('zlib');

const app = express();

app.use(compression({
  // Only compress responses larger than 1KB
  // Compressing tiny responses wastes CPU for negligible gain
  threshold: 1024,

  // Compression level: 1 (fast, less compression) to 9 (slow, max compression)
  // Level 6 is the sweet spot for most APIs
  level: zlib.constants.Z_DEFAULT_COMPRESSION,

  // Don't compress responses that are already compressed
  // (images, videos, zipped files — compression can actually make them bigger)
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    // Use the default filter as a fallback
    return compression.filter(req, res);
  }
}));

// Your routes here
app.get('/api/products', async (req, res) => {
  const products = await db.getProducts();
  res.json(products);
});

app.listen(3000);

The threshold option is important. Compressing a 200-byte response uses more CPU than you save in bandwidth — it's like hiring a professional packer to compress a sticky note. Set a sensible floor (1KB is a good default).

Real Numbers: What Does This Actually Save?

Let me give you a concrete example. A typical paginated API response with 50 user objects might look like this:

Format Size
Raw JSON 48 KB
gzip (level 6) 9 KB
Brotli 7.5 KB

That's an 80% reduction. On an API that gets 1 million requests a day, you're looking at saving roughly 37 GB of outbound bandwidth — daily. At typical cloud egress prices, that's real money.

Your users also notice the speed difference. Smaller payloads mean faster time-to-first-byte on mobile networks, and that directly affects perceived performance scores.

What NOT to Compress

A common mistake is blindly compressing every response. Some content types are already compressed and will actually get bigger if you try to compress them again:

  • Images (JPEG, PNG, WebP, AVIF) — already use lossy/lossless compression
  • Videos (MP4, WebM) — same story
  • ZIP / GZIP files — compressing a compressed file is pointless
  • PDFs — often contain compressed streams internally

The compression middleware is smart enough to skip these by default (it checks the Content-Type header), but if you're building a custom pipeline, keep this in mind.

Also: don't compress tiny responses. The overhead of the compression handshake and CPU cost isn't worth it for payloads under ~1KB.

A Word About Reverse Proxies

If you're running Nginx or a load balancer in front of your Node.js app, you might prefer to let Nginx handle compression. It's highly optimized for this, has built-in Brotli support (via a module), and offloads the CPU work from your Node.js process.

In that case, don't add compression middleware — let the proxy do it. Just make sure it's actually configured, because "I'll set that up later" has a funny way of meaning "never."

If you're running Node.js directly exposed (serverless, small hobby projects, internal tools), add compression middleware. It's two lines of code for an immediate win.

The Bottom Line

Response compression is one of those rare optimizations that's:

  • Free in terms of development effort (two lines of code)
  • Significant in terms of impact (60-80% bandwidth reduction)
  • Invisible to your API consumers (clients decompress transparently)

There's no excuse to ship an Express API without it. Add compression middleware today, deploy it, and then feel smug watching your bandwidth metrics drop like a stone.

Your users' mobile connections will thank you. Your cloud bill will thank you. Future-you will thank you.


What performance quick-wins have you shipped that felt almost too easy? Drop them in the comments — I'm collecting a list of "embarrassingly simple fixes with outsized impact" and this one is definitely on it.