Skip to main content

HTTP Compression

The HTTP listener (--listen 'http://...?api=...') compresses response bodies when the client asks for one of the codings below, and decompresses request bodies sent with one of them, for every endpoint. There is nothing to configure: every coding is always available in both directions, and a request that asks for none is answered uncompressed.

CodingTokenNotes
Zstandardzstdbest ratio, and fast; the default pick
Brotlibrsupported by every modern browser; compresses at quality 5
gzipgzipunderstood by every HTTP client and browser; x-gzip is accepted as the same coding
Deflatedeflatethe zlib-wrapped format RFC 9110 defines; a request body in raw deflate, which some clients send under this name, is accepted too
ZXCzxcserenedb/zxc, the fastest decode; not an IANA-registered coding, so only clients that opt in ask for it
LZ4 framelz4fastest to compress; same caveat as zxc
Snappysnappythe raw block format, as Prometheus remote write sends it; a body is compressed or decompressed whole, so a streamed response is sent only once it is complete

Server preference is the order above: zstd, br, gzip, deflate, zxc, lz4, snappy. It picks between codings the client accepts equally: the client's own q weights come first, so Accept-Encoding: gzip;q=1.0, zstd;q=0.1 answers gzip.

A response is compressed when all of these hold:

  • the client's Accept-Encoding asks for one of them (q weights and * are honoured; a coding the client did not list is used only through *);
  • the response has a body: not HEAD, not 1xx/204/304;
  • the body is at least 1 KiB, or its length is not known before it is sent.

A body the server sends whole carries the compressed Content-Length; when the encoding does not make it smaller, the body is sent as-is instead. A body the server streams is sent as Transfer-Encoding: chunked once compressed, since its compressed length is not known up front.

Compressed responses carry Content-Encoding: <token> and Vary: Accept-Encoding.

Compression level​

A client can pick the level by writing it in parentheses after the coding: Accept-Encoding: zstd(1) asks for the fastest zstd, gzip(9);q=0.5, br(4) for brotli at quality 4. This is a SereneDB extension, so standard clients never send it; without a level, or with level 0, each coding uses its default. A level outside the range below answers 400 Bad Request with {"error": "unsupported_compression_level"}, even when the header also lists another coding. snappy has no levels, so any level but 0 is rejected too. A malformed level (zstd(x), zstd(1, a level without a coding) also answers 400 Bad Request. The response names the bare coding (Content-Encoding: zstd), since decoding does not depend on the level.

The ranges stop below each library's maximum. Higher levels compress a few percent better but cost far more memory or CPU per response: zstd 22 needs over 800 MiB for a streamed response, and brotli 11 compresses at about 1 MB/s.

CodingLevelsDefault
zstd-131072 (fastest) to 83
br1 to 65
gzip, deflate1 to 96
zxc1 to 73
lz40 (fast) to 9 (high compression)0

A request body's Content-Encoding may carry a level too: a numeric level is ignored, and a malformed one (gzip(x)) answers 415 Unsupported Media Type.

Compressed request bodies​

A request body sent with Content-Encoding is decompressed once the request is authenticated, before it reaches the endpoint, so every API accepts compressed bodies the same way. The field may list up to two codings in the order they were applied (Content-Encoding: gzip, zstd); identity is ignored, and the tokens are case-insensitive. A decompressed body is held to the same size limit as an uncompressed one: 100 MiB for es and otel, and 4 MiB for mcp, and the largest of them on a listener that serves several. A zstd body must use a window of at most 8 MiB, the limit RFC 8878 sets for HTTP: levels 1 to 19 always fit. --ultra levels, --long and a larger --window ask for a larger window, which zstd shrinks only when it knows while compressing that the input is smaller, as for a file but not for a pipe; a frame whose window is still above 8 MiB is rejected, however small its content.

Rejected requests​

Accept-EncodingResponse
absent, empty, or asking only for codings we do not have (compress)200, uncompressed
identity;q=0 or *;q=0, with no coding we have left acceptable406 Not Acceptable: nothing can be sent
a weight that is not a qvalue (gzip;q=huh, gzip;q=2, gzip;q=nan)400 Bad Request
a level outside the coding's range (zstd(22), snappy(1))400 Bad Request
Content-EncodingResponse
a coding we do not have (compress), or more than two codings415 Unsupported Media Type
a malformed level (gzip(x))415 Unsupported Media Type
a body that is corrupt or truncated for its coding, or a zstd window above 8 MiB400 Bad Request
a body that decompresses past the body size limit413 Content Too Large

The Accept-Encoding errors are answered before authentication, so a request without credentials gets them instead of 401. The Content-Encoding errors are answered after authentication and before the endpoint runs. Both come as {"error": "<reason>"}, and the connection stays usable for the next request.

Limitations​

  • HEAD is never compressed. A HEAD answers with the uncompressed Content-Length and no Content-Encoding, while the matching GET may answer with a compressed body of another length, or chunked. A client must not size a resource with HEAD and expect the GET body to match.