Skip to content

Request Decompression & Response Compression

Key Points

  • UseRequestDecompression (.NET 7+) — automatically decompresses request bodies when the client sends Content-Encoding: gzip|br|deflate. Off by default.
  • UseResponseCompression — compresses response bodies based on Accept-Encoding. Brotli > Gzip for text; configure providers explicitly.
  • Don't compress already-compressed payloads — JPEGs, MP4s, PNGs gain nothing and waste CPU. Filter by Content-Type (default does this for common image/video types).
  • TLS termination in proxy? — many reverse proxies (nginx, AGW, AFD) compress for you; double-compressing wastes CPU. Configure one or the other.
  • Senior risk awareness: response compression with HTTPS once enabled the BREACH attack family. Don't compress responses that contain user-controlled secrets in the same payload.

Concepts (deep dive)

Request decompression

Clients posting large JSON / files compress the body to save bandwidth:

POST /api/upload HTTP/1.1
Content-Encoding: gzip
Content-Type: application/json

<gzipped bytes>

Without UseRequestDecompression, your handler reads the gzip bytes — model binding fails. With it, the body is transparently decompressed on read.

builder.Services.AddRequestDecompression();    // default providers: gzip, br, deflate

var app = builder.Build();
app.UseRequestDecompression();

Custom provider:

builder.Services.AddRequestDecompression(opts =>
{
    opts.DecompressionProviders.Add("custom", new MyDecompressionProvider());
});

Limits to be aware of: - Max input length controlled via Kestrel's MaxRequestBodySize. - Decompression bomb risk — a small gzip can expand to gigabytes. Set body size limits AND decompressed limits.

Response compression

builder.Services.AddResponseCompression(opts =>
{
    opts.EnableForHttps = true;          // BREACH-aware; only enable knowing risk
    opts.Providers.Add<BrotliCompressionProvider>();
    opts.Providers.Add<GzipCompressionProvider>();
    opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
});
builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Optimal);
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);

var app = builder.Build();
app.UseResponseCompression();

Brotli vs Gzip

Text (10 KB JSON):
  Gzip Fastest:    2.4 KB    50 µs
  Gzip Optimal:    1.9 KB    300 µs
  Brotli Fastest:  1.7 KB    100 µs
  Brotli Optimal:  1.4 KB    1 ms

Brotli compresses text better than gzip; both are well-supported in browsers. Default to Brotli + Gzip fallback; pick CompressionLevel.Fastest unless throughput justifies the CPU.

When NOT to compress

  • Already-compressed: JPEG, PNG, GIF, MP4, MP3, ZIP, GZ, BR — compressing wastes CPU, sometimes grows.
  • Real-time / latency-sensitive: low-RTT internal services may not benefit from the CPU cost.
  • Below 1-2 KB: overhead exceeds savings.
  • HTTPS + user-content reflection: BREACH attack risk (see security section).

ResponseCompressionDefaults.MimeTypes excludes media types by default; verify your app doesn't add them back.

BREACH attack briefing

If a TLS response contains: 1. A secret (CSRF token, session ID). 2. Attacker-controlled content reflected in the response. 3. Compression enabled.

The attacker can guess the secret one character at a time by observing compressed length differences. Mitigations: - Don't include secrets in compressed responses, or - Add randomness to responses (XSRF token randomized per request), or - Disable response compression for endpoints that include secrets.

Streaming and compression

Compression buffers internally. With chunked transfer or streaming, responses are compressed on the fly. Be aware: - Content-Length is not known up front; chunked encoding used. - Compression buffer sizes affect TTFB.

For Server-Sent Events / streaming JSON / huge file downloads, often skip compression to keep flush latency low.

Reverse proxy doing compression

If nginx / AGW / Cloudfront already compresses, configuring UseResponseCompression in the app double-compresses or wastes CPU — only one layer should compress. Common pattern:

Internal service → ALB (no compression) → app (compresses) → client
                ↑ direct
or
Internal service → AFD (compresses) → app (no compression) → client

Pick one. The proxy is usually faster (native code) but app-level compression is portable across deploys.

Custom compression provider

public class ZstdCompressionProvider : ICompressionProvider
{
    public string EncodingName => "zstd";
    public bool SupportsFlush => true;
    public Stream CreateStream(Stream outputStream) => new ZstdNet.CompressionStream(outputStream);
}

Useful if a client asserts a non-standard encoding (private intranet pairs).


Code: correct vs wrong

❌ Wrong: enabling compression for everything

builder.Services.AddResponseCompression();        // defaults are sane but...
opts.EnableForHttps = true;
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes
    .Concat(["image/jpeg", "video/mp4"]);          // wastes CPU

✅ Correct: text + JSON, default exclusions

builder.Services.AddResponseCompression(opts =>
{
    opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
    opts.EnableForHttps = true;
});

❌ Wrong: forgetting decompression bomb limits

builder.WebHost.ConfigureKestrel(opts => opts.Limits.MaxRequestBodySize = 100 * 1024 * 1024); // 100 MB
// + UseRequestDecompression — a 10 MB gzip can expand to 10 GB

✅ Correct: bound decompressed size

app.Use(async (ctx, next) =>
{
    ctx.Request.EnableBuffering(bufferThreshold: 64 * 1024, bufferLimit: 100 * 1024 * 1024);
    await next();
});

Or read the request body manually with a length-limited stream.


Design patterns for this topic

Pattern 1 — Brotli first, gzip fallback

builder.Services.AddResponseCompression(opts =>
{
    opts.Providers.Add<BrotliCompressionProvider>();
    opts.Providers.Add<GzipCompressionProvider>();
});

Order matters: first matching Accept-Encoding wins.

Pattern 2 — Skip compression on streaming endpoints

app.MapGet("/stream", async (HttpContext ctx) =>
{
    ctx.Features.Get<IHttpResponseCompressionFeature>()!.DisableCompression();
    // stream response
});

Pattern 3 — Validate content-length before decompression

builder.Services.AddRequestDecompression(opts => { /* defaults */ });
app.Use(async (ctx, next) =>
{
    if (ctx.Request.ContentLength is > 50 * 1024 * 1024)   // 50 MB compressed cap
    {
        ctx.Response.StatusCode = 413; return;
    }
    await next();
});

Pattern 4 — Disable for sensitive endpoints

For endpoints that return user-context-mixed-with-secrets, opt out:

[ResponseCompressionExempt]   // your own attribute checked in middleware
public IActionResult GetSensitive() => Ok(...);

Pros & cons / trade-offs

Choice Pros Cons
Compress text/JSON Big bandwidth savings Small CPU cost
Compress everything Simple config CPU waste on media
Brotli optimal Smallest output High CPU per request
Brotli fastest Good ratio, low CPU Slightly bigger than optimal
Proxy-level only Native perf, central Must coordinate with deploys
App-level only Portable CPU on app servers

When to use / when to avoid

  • Use response compression for text, JSON, HTML, CSS, JS, XML.
  • Use request decompression for APIs accepting large payloads from compression-aware clients.
  • Avoid for media types, streaming, latency-critical endpoints, BREACH-vulnerable secrets.
  • Avoid doubling proxy + app compression.

Interview Q&A

Q1. Why is EnableForHttps = false the default? A. BREACH attack: an attacker who can inject content into a TLS-compressed response and observe its length can extract secrets character-by-character. Microsoft made you opt-in so you confirm you've thought about it.

Q2. Brotli vs Gzip — when each? A. Browsers support both. Brotli compresses better, slightly slower. Use Brotli + Gzip fallback. CompressionLevel.Fastest covers most cases; Optimal only if bandwidth >> CPU.

Q3. What's a decompression bomb? A. A small compressed payload that expands to a huge size — designed to exhaust memory/CPU on the server. Mitigate by bounding both MaxRequestBodySize (compressed) and decompressed read limits.

Q4. When skip compression? A. Already-compressed media (JPEG, MP4), tiny payloads (<1 KB), streaming endpoints (latency), and BREACH-sensitive responses with reflected user input + secrets.

Q5. How does the middleware decide what to compress? A. Match Accept-Encoding against registered providers; check Content-Type against allowed MIME list; verify response is large enough; skip if response stream is already encoded.

Q6. What does EnableForHttps = true change? A. Allows compression on HTTPS responses; default is HTTP only. Required behind HTTPS-terminated reverse proxies that re-encrypt to upstream.

Q7. Should the reverse proxy or the app compress? A. Either, not both. Proxy compression is faster (native code, central) but couples deploys. App compression is portable. Pick one and disable the other.

Q8. Can request decompression interact with rate limiting / size validation? A. Yes — order matters. Run size checks against the compressed body first; the request body limit middleware sees the wire bytes. Decompression then expands but you can apply additional length limits inside the handler.


Gotchas / common mistakes

  • MaxRequestBodySize bounding compressed size only — decompressed payload can be 10×+ larger.
  • Compressing media (JPEG/MP4) — wasted CPU, sometimes larger output.
  • Both proxy and app compressing — recompresses; sometimes results in worse ratios.
  • BREACH-vulnerable responses — secrets + reflected user input + compression on HTTPS.
  • Streaming responses with compression — buffers; first-byte latency.
  • Custom MIME types not in default whitelist — silent no-compress.
  • EnableForHttps = true without thinking through BREACH.
  • Forgetting Vary: Accept-Encoding (added automatically) — caches must vary.

Further reading