Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

Response Compression

app.UseResponseCompression();

That is the whole setup. Brotli, gzip and deflate are negotiated from Accept-Encoding, and text-ish content types are compressed.

This is worth more here than in a datacentre. An embedded server is reached over cellular or a tunnel, where the CPU is idle and the bytes are not.

builder.Services.AddResponseCompression(o =>
{
o.Level = CompressionLevel.Optimal;
o.MinimumBytes = 512;
o.MimeTypes.Add("application/vnd.myapp+json");
});
app.UseResponseCompression();

Or inline, for a server with no container:

app.UseResponseCompression(o => o.Level = CompressionLevel.SmallestSize);
Property Default Notes
Providers brotli, gzip, deflate Best first; remove one to stop offering it
MimeTypes text/*, JSON, XML, JS, wasm, SVG, ndjson Exact, or type/* prefix
ExcludedMimeTypes text/event-stream Never compressed
MinimumBytes 1024 Only applies when the length is known up front
Level Fastest
EnableForHttps true See below
ShouldCompress null A predicate that overrides all of the above

MimeTypes is everything text or text-shaped. Images, video, fonts and archives are already compressed, and running them through gzip spends CPU to make them very slightly larger. MinimumBytes exists for the same reason at the other end: below roughly a packet’s worth there is nothing to win.

text/event-stream is excluded because compressing it is legal and usually wrong — the compressor buffers, and a stream whose whole purpose is immediacy arrives in bursts instead.

Accept-Encoding is parsed with q-values. q=0 is a refusal, not a weak preference. Higher q wins; equal q falls back to the provider’s own priority, which is how gzip, br — no stated preference — still picks brotli. A bare * accepts anything not otherwise named, so the server’s first choice applies.

The decision is made on the first byte written, not when the middleware runs — nothing has set a content type yet at that point.

It works by wrapping the response body control, so every write path funnels through one place: a handler using Body, BodyWriter, WriteAsync, a result type or the static file middleware is covered without knowing this exists.

Compressing clears the declared Content-Length, since the compressed size is unknown until the last block. Ranges, pre-encoded bodies, 204/304, HEAD and already-compressed media types are all passed through untouched.

Vary: Accept-Encoding is appended from an OnStarting callback either way — appended rather than set, because a handler’s own Vary is not something to trample, and from a callback because setting it before the handler runs would let the handler overwrite it.

Put it early. It has to wrap everything that writes a body, including static files:

app.UseCors();
app.UseResponseCompression();
app.UseStaticFiles("./wwwroot");

EnableForHttps is on by default, because a tunnelled server is HTTPS end to end and switching compression off there would mean never compressing at all.

For content published with .br/.gz sidecars — a Blazor publish does this — serving those beats recompressing per request: they were compressed once at maximum effort, and this middleware runs at a level chosen for speed. Turn on ServePrecompressedFiles in static files (and Blazor does it for you).