.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

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