Back to blog
Backend Systemsintermediate

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.

LearnixoApril 15, 20265 min read
.NETC#PerformanceCompressionASP.NET Core
Share:𝕏

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

C#
// 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

C#
// CompressionLevel enum options:
// Fastest      — minimal CPU, less compression
// Optimal      — balanced (default)
// SmallestSize — maximum compression, highest CPU cost
// NoCompression — skip compression

For 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

C#
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:

C#
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:

C#
[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 encoding

If Content-Encoding is absent, compression isn't firing. Common causes:

  1. UseResponseCompression() is after MapControllers() in the pipeline
  2. EnableForHttps is false (default) and you're testing on HTTPS
  3. 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:

C#
[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.Fastest for high-throughput APIs; SmallestSize for batch export endpoints
  • Enable EnableForHttps = true for 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?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.