.NET & C# Development · Lesson 65 of 92
Compress API Responses — Halve Your Payload Size
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