Compression & Payload Tuning
Every byte your server sends over the wire costs bandwidth and latency, and every byte it receives costs CPU to parse and memory to buffer. Two cheap, high-leverage optimizations address both sides: compressing responses so they travel smaller, and bounding request bodies so a single oversized payload cannot exhaust memory or block the event loop. NestJS sits on top of Express or Fastify, so you tune both concerns through the underlying HTTP platform while keeping your controllers untouched.
Enabling gzip compression on Express
The default Express platform integrates with the standard compression middleware. It inspects the client’s Accept-Encoding header and transparently gzips (or deflates) eligible responses, setting Content-Encoding for you.
npm install compression
npm install --save-dev @types/compression
Register it globally in main.ts before the app starts listening.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import * as compression from 'compression';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
compression({
threshold: 1024, // only compress responses larger than 1 KB
level: 6, // zlib level 0-9: balance of CPU vs ratio
}),
);
await app.listen(3000);
}
bootstrap();
The threshold matters: compressing a 200-byte JSON response wastes CPU and can even make the payload larger after headers. A 1 KB floor skips tiny bodies entirely.
Tip: In production, terminate compression at a reverse proxy (nginx, an ALB, or a CDN) when you can. Offloading it from Node frees the event loop for request handling. Use in-app compression mainly when no such layer exists.
Compression on Fastify
If you use the Fastify adapter, install the dedicated plugin instead. Fastify’s @fastify/compress supports both gzip and brotli, and brotli typically yields 15-25% smaller payloads for text at a similar CPU cost.
npm install @fastify/compress
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(compression, {
encodings: ['br', 'gzip'], // prefer brotli, fall back to gzip
threshold: 1024,
});
await app.listen(3000, '0.0.0.0');
}
bootstrap();
Encoding comparison
| Encoding | Ratio (text) | CPU cost | When to prefer |
|---|---|---|---|
| gzip | Good | Low | Universal client support, proxy offload |
| deflate | Good | Low | Legacy fallback only |
| brotli | Best | Higher | Static assets, Fastify, brotli-aware clients |
Tuning request body limits
By default Express caps JSON bodies at 100 KB. APIs that accept file uploads, bulk imports, or large documents need a higher limit, while public endpoints benefit from a tight cap that rejects abusive payloads early. Configure the body parser when creating the app.
// src/main.ts
import { json, urlencoded } from 'express';
const app = await NestFactory.create(AppModule);
app.use(json({ limit: '5mb' }));
app.use(urlencoded({ extended: true, limit: '5mb' }));
On Fastify, set bodyLimit on the adapter (bytes), and the platform enforces it before your handler runs:
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ bodyLimit: 5 * 1024 * 1024 }),
);
When a client exceeds the limit, the framework rejects the request automatically with a clear status before your controller is ever invoked.
Output:
HTTP/1.1 413 Payload Too Large
Content-Type: application/json
{"statusCode":413,"message":"request entity too large"}
Streaming large responses
Compression and body limits cover ordinary JSON. For genuinely large outputs (a report export, a generated file, a media asset), buffering the whole thing in memory before sending defeats the purpose. Stream it instead with StreamableFile, which pipes a readable stream straight to the socket and keeps memory flat regardless of file size.
// src/files/files.controller.ts
import { Controller, Get, StreamableFile, Header } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('export')
@Header('Content-Type', 'application/octet-stream')
@Header('Content-Disposition', 'attachment; filename="report.csv"')
download(): StreamableFile {
const stream = createReadStream(join(process.cwd(), 'report.csv'));
return new StreamableFile(stream);
}
}
Warning: Do not double-compress already-compressed payloads. Images, video, and
.zip/.gzfiles gain nothing from gzip and only burn CPU. Thecompressionmiddleware honours afilterfunction — use it to skip such content types, or setContent-Encodingon the stream yourself.
Best Practices
- Set a
threshold(around 1 KB) so tiny responses skip compression overhead. - Prefer offloading compression to a reverse proxy or CDN in production; reserve in-app compression for environments without one.
- Use brotli for static, cacheable text assets and gzip for dynamic responses with broad client support.
- Size body limits per route group: generous for upload endpoints, tight for public JSON APIs.
- Stream large or unbounded responses with
StreamableFileinstead of buffering them in memory. - Never compress already-compressed media; filter those content types out to save CPU.