Response Compression — Cut Your API Payload Size in Half
Enable Brotli and Gzip compression in ASP.NET Core, choose the right compression level, know what not to compress, and measure the actual bandwidth savings.
Why Bother?
A typical JSON API response compresses 60-80%. A 200 KB payload becomes 40-50 KB. Across thousands of requests per minute, that's real bandwidth saved and real latency improvement — especially for mobile clients on variable connections.
Enable Compression
// Program.cs
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true; // must opt in explicitly for HTTPS
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
["application/json", "application/problem+json"]);
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest; // see trade-offs below
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.SmallestSize;
});
var app = builder.Build();
app.UseResponseCompression(); // must come before UseRouting / MapControllers
app.MapControllers();The middleware negotiates with the client via Accept-Encoding header. Brotli (br) is tried first — it compresses 15-20% better than Gzip. Clients that don't support Brotli fall back to Gzip automatically.
Compression Levels
// CompressionLevel enum options:
// Fastest — minimal CPU, less compression
// Optimal — balanced (default)
// SmallestSize — maximum compression, highest CPU cost
// NoCompression — skip compressionFor an API serving 1000 req/s, Fastest is almost always the right call. The CPU difference between Fastest and SmallestSize is significant; the size difference is 5-10%. You're trading CPU cycles that could be serving more requests for marginal extra bandwidth savings.
Rule of thumb:
- APIs under load:
Fastest - Background export endpoints (large files, low concurrency):
SmallestSize
What to Compress
options.MimeTypes = new[]
{
// YES — compress these
"application/json",
"application/problem+json",
"text/plain",
"text/html",
"text/css",
"text/javascript",
"application/javascript",
"application/xml",
"text/xml",
// NO — do not add these (already compressed)
// "image/jpeg"
// "image/png"
// "image/webp"
// "application/zip"
// "application/pdf"
// "video/mp4"
};Compressing already-compressed formats wastes CPU and often increases payload size. ASP.NET Core's default MIME type list is conservative — you need to explicitly add application/json.
When NOT to Compress
Small responses. Compression has per-response overhead — headers, CPU time. For responses under ~1 KB, the overhead exceeds the savings. The middleware has a threshold:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// Default minimum is 1 byte — responses this small won't benefit.
// The middleware applies compression regardless of size by default;
// add a custom provider to skip tiny responses:
});You can implement a custom ICompressionProvider wrapper that skips compression below a size threshold, or simply accept that very small responses get a tiny overhead.
Server-Sent Events / streaming responses. Compression buffers the response to compress it — this breaks streaming. Exclude SSE endpoints:
[HttpGet("events")]
public async Task StreamEvents(CancellationToken ct)
{
Response.Headers.ContentType = "text/event-stream";
Response.Headers.CacheControl = "no-cache";
// Disable compression for this response
var feature = HttpContext.Features.Get<IHttpsCompressionFeature>();
if (feature != null) feature.Mode = HttpsCompressionMode.DoNotCompress;
while (!ct.IsCancellationRequested)
{
await Response.WriteAsync($"data: {DateTime.UtcNow}\n\n", ct);
await Response.Body.FlushAsync(ct);
await Task.Delay(1000, ct);
}
}gRPC. gRPC handles its own compression at the protocol level. Don't layer ASP.NET Core response compression on top.
Verifying It Works
Check in browser DevTools (Network tab):
Request Headers:
Accept-Encoding: gzip, deflate, br
Response Headers:
Content-Encoding: br
Content-Length: 4821 ← compressed size
Vary: Accept-Encoding ← tells CDNs to cache per encodingIf Content-Encoding is absent, compression isn't firing. Common causes:
UseResponseCompression()is afterMapControllers()in the pipelineEnableForHttpsisfalse(default) and you're testing on HTTPS- The MIME type isn't in the allowed list
CRIME/BREACH Attacks
EnableForHttps = false is the default because compressing secrets over HTTPS can leak them via CRIME/BREACH attacks. These attacks require the attacker to inject data into the same response as a secret (e.g., CSRF token) and observe compressed sizes.
In practice: if your API returns only JSON data (not HTML with embedded tokens), BREACH doesn't apply. Enable EnableForHttps = true for pure JSON APIs. Keep it off for endpoints that mix user-controlled input with secrets in the same response.
Benchmark: Payload Size
A real 100-record order list JSON response:
| Encoding | Size | Ratio | |---|---|---| | None | 48,240 bytes | 1.0x | | Gzip (Fastest) | 6,102 bytes | 7.9x | | Gzip (SmallestSize) | 5,618 bytes | 8.6x | | Brotli (Fastest) | 5,487 bytes | 8.8x | | Brotli (SmallestSize) | 4,821 bytes | 10.0x |
Brotli Fastest vs Gzip Fastest: comparable size, slightly higher CPU. Brotli SmallestSize vs Gzip SmallestSize: ~14% smaller. For most APIs, Brotli Fastest is the optimal choice.
[ResponseCache] vs Compression
These are independent concerns — they compose:
[ResponseCache(Duration = 300, VaryByHeader = "Accept-Encoding")]
[HttpGet("products")]
public async Task<IActionResult> GetProducts()
{
// Response is compressed AND cached
// VaryByHeader ensures Brotli-capable clients don't get Gzip-cached response
return Ok(await _repo.GetAllAsync());
}Always include Vary: Accept-Encoding in cached compressed responses so CDNs and proxies serve the correct encoding to each client.
Summary
AddResponseCompression()+UseResponseCompression()— two lines, done- Add Brotli first, then Gzip — clients negotiate the best they support
- Compress JSON, not images/zip/video — they're already compressed
CompressionLevel.Fastestfor high-throughput APIs;SmallestSizefor batch export endpoints- Enable
EnableForHttps = truefor JSON APIs; leave it off if responses mix user input with secrets - A typical JSON payload shrinks 80% — the bandwidth savings compound across every request
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.