mrkeyoor.com_
Thu 06 Aug 05:53 UTC
npmWeb Backendupdated 06 Aug 2026

compression

compression is the Express middleware that gzips, deflates, or brotli-compresses your HTTP responses on the way out. You app.use() it once and it inspects each response: it reads the client's Accept-Encoding header, picks an encoding, checks that the Content-Type looks compressible via the compressible module, checks that the body is over a size threshold, then swaps in a zlib stream and sets Content-Encoding and Vary for you. It also skips anything marked Cache-Control: no-transform, and adds a res.flush() method so streaming endpoints can push buffered bytes to the client on demand.

Verdict

The right answer when Node itself is the edge and your responses are dynamic, and it has been stable enough that a one-line app.use() is genuinely all most apps need. Put a proxy or CDN in front instead if you have one, because compressing in JavaScript is the most expensive place to do it.

API stability5/5Still 1.x after more than a decade; the middleware signature, the options object, compression.filter, and res.flush have not changed shape, and recent releases only added options like brotli and enforceEncoding
Docs4/5The README documents every option with its type and default, links the underlying zlib docs, and includes worked examples for Express, bare node:http, and the server-sent events flush problem; there is no separate docs site and nothing on how it interacts with proxies
Maintenance4/5Part of the Express organization with a push on 2026-08-05 and only 11 open issues, but releases are infrequent (1.8.1 shipped July 2025) and it still carries a debug@2.6.9 pin from 2017
Ecosystem5/5About 39M downloads a week and the assumed default in nearly every Express tutorial and boilerplate; the compressible module it uses for content-type decisions is maintained by the same organization

Use it if

  • You serve JSON or HTML from a Node process that faces clients directly, with no nginx, Caddy, or CDN in front doing the compression
  • Your responses are dynamic and large enough to matter (API payloads, server-rendered pages) so precompressing at build time is not an option
  • You want per-route control over what gets compressed, which the filter option and Cache-Control: no-transform both give you
  • You are already on Express or any connect-style stack and want a one-line change rather than a proxy configuration
Skip it if

Setup reality

npm install compression is quick, but the module is old-style CommonJS with no exports map and no bundled types, so TypeScript users also need @types/compression and esModuleInterop. Placement decides whether it does anything at all: app.use(compression()) must run before the routes and static handlers whose output you want compressed. The 1kb default threshold is only reliable when Content-Length is known at header time, otherwise the middleware assumes the body is over the limit. Brotli quality defaults to 4 rather than zlib's usual tradeoffs, and brotli is preferred over gzip when the client offers both. The dependency tree still pins debug@2.6.9, which shows up in audits and dedupe reports even though nothing is broken by it.

Patterns

Compress every response in an Express appbasic-express

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

const app = express()

app.use(compression())

app.get('/api/items', (req, res) => {
  res.json(items)
})

app.listen(3000)

Order decides everything: middleware registered before compression writes its response without ever passing through it, so put this above your routes and static handlers.

Opt individual requests out with a filtercustom-filter

const compression = require('compression')

function shouldCompress (req, res) {
  if (req.headers['x-no-compression']) return false
  return compression.filter(req, res)
}

app.use(compression({ filter: shouldCompress }))

Always fall through to compression.filter instead of returning true, or you start gzipping already-compressed content types like images and video and make responses larger.

Skip compression for a single responseskip-one-route

app.get('/download/:id', (req, res) => {
  // never compressed: the middleware honours no-transform
  res.setHeader('Cache-Control', 'no-transform')
  res.setHeader('Content-Type', 'application/octet-stream')
  streamFile(req.params.id).pipe(res)
})

Cache-Control: no-transform is the per-response escape hatch and also tells intermediary caches not to recompress. It is more reliable than path checks in the filter when routes are mounted under changing prefixes.

Trade CPU against payload sizetune-level-threshold

app.use(compression({
  level: 6,          // 0 none, 1 fastest, 9 smallest, -1 zlib default
  threshold: '4kb',  // skip bodies smaller than this
  memLevel: 8
}))

The threshold is advisory: if Content-Length is unknown when headers are written, the response is assumed to be over it and gets compressed anyway. Set Content-Length on small responses you want left alone.

Configure brotli separately from gzipbrotli-options

const zlib = require('zlib')

app.use(compression({
  brotli: {
    params: {
      [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
      [zlib.constants.BROTLI_PARAM_SIZE_HINT]: 0
    }
  }
}))

The middleware sets brotli quality to 4 by default, not zlib's own default of 11, because 11 is far too slow per request. Raise it deliberately and measure; brotli cost climbs steeply above quality 6.

Keep server-sent events flowingserver-sent-events

app.use(compression())

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')

  const timer = setInterval(() => {
    res.write('data: ping\n\n')
    res.flush()   // without this the client sees nothing
  }, 2000)

  res.on('close', () => clearInterval(timer))
})

Compression needs a window of output before it emits anything, so an event stream stalls silently until the buffer fills. res.flush() is added to the response by this middleware and does not exist without it.

Use it without Expressbare-http-server

const http = require('http')
const compress = require('compression')({ threshold: 0 })

const server = http.createServer((req, res) => {
  compress(req, res, (err) => {
    if (err) {
      res.statusCode = err.status || 500
      res.end(err.message)
      return
    }
    res.setHeader('Content-Type', 'text/plain')
    res.end('hello world!')
  })
})

server.listen(3000)

It is plain (req, res, next) middleware, so any connect-style stack works. You call it yourself and handle the error argument, which Express normally does for you.

Handle clients that send no Accept-Encodingenforce-encoding

app.use(compression({
  enforceEncoding: 'gzip'   // default is 'identity'
}))

This only applies when the request has no Accept-Encoding header at all. Old HTTP clients and some load-balancer health checks fall into that bucket, and forcing gzip on a client that cannot decode it produces unreadable bytes rather than an error.

Wire it up in TypeScripttypescript-setup

// npm i -D @types/compression
import express from 'express'
import compression from 'compression'

const app = express()
app.use(compression({ threshold: 1024 }))

No types ship with the package, so without @types/compression this is an implicit any. The default import needs esModuleInterop or allowSyntheticDefaultImports because the module only sets module.exports.

Check that it is actually compressingverify-encoding

$ curl -sI -H 'Accept-Encoding: br, gzip' http://localhost:3000/api/items
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Vary: Accept-Encoding
Content-Encoding: br

$ curl -sI -H 'Accept-Encoding: gzip' http://localhost:3000/api/items | grep -i content-encoding

No Content-Encoding header means one of four things: the body was under the threshold, the Content-Type was not compressible, a filter said no, or the middleware runs after the route. Check them in that order.

Understand the Vary header it setsvary-header-caching

// compression() adds this to every response it considers:
//   Vary: Accept-Encoding
// so shared caches store one entry per encoding

app.use(compression())
app.use(express.static('public', {
  setHeaders: (res) => res.setHeader('Cache-Control', 'public, max-age=3600')
}))

Vary is added even when the response ends up uncompressed, which is correct but multiplies cache keys in front of you. Dropping or overwriting Vary downstream is how clients end up served brotli bytes they never asked for.

Alternatives

PackageRegistryPick it when
@fastify/compressnpmYou are on Fastify and want compression wired into its hook and payload system instead of connect middleware
express-static-gzipnpmYour compressible traffic is static assets you can precompress at build time and serve as .gz or .br files
http-compressionnpmYou want gzip and brotli on a bare node:http or framework-agnostic server without pulling in the connect middleware style