---
title: "aadhar.sh/garage/compression teardown"
description: "Shipping brotli q11 and zstd dictionary deltas: what the edge really does with Accept-Encoding, and the one-line bug that impersonated three platform limits."
path: "/garage/compression"
section: "garage"
kind: "content"
updated: "2026-07-27"
source: "https://aadhar.sh/garage/compression"
---

> Site index: https://aadhar.sh/llms.txt
> Section index: https://aadhar.sh/garage/llms.txt
> This is the Markdown twin of a page on aadhar.sh. The HTML at the source
> URL below is the original, and is hand-written and unminified on purpose.

# Compression teardown shipped

Putting a site behind a [CDN][cdn] is supposed to end the compression conversation. The edge negotiates, the edge compresses, you stop thinking about it. That held here for a year, and then I measured it: the edge was handing browsers **more** bytes than its own second choice would have, and compressing at roughly a third of the effort the same algorithm can reach offline.

Fixing that took four rounds, three of which I spent blaming the platform for a bug in my own code. This page keeps the wrong turns, because the wrong turns are the reusable part.

## What the edge was actually doing

Cloudflare prefers [zstd][zstd] when a browser offers everything. Reasonable default. On this site it lost to [brotli][brotli] on every asset I checked, because these are small text documents and brotli's static dictionary is tuned for exactly that:

| document | raw | what browsers got | brotli q11, offline | gap |
| --- | --- | --- | --- | --- |
| homepage | 47,399 | 13,264 zstd | 10,524 | 20.7% |
| /lens | 40,923 | 11,625 | 8,835 | 24.0% |
| nav.js | 46,268 | 15,909 | 13,047 | 18.0% |
| luna.css | 36,980 | 9,522 | 7,590 | 20.3% |

Two separate losses hide in that table. The edge picked the weaker algorithm, and it compressed at about quality 4, because it is compressing on the fly for everyone and cannot afford quality 11. Neither is a mistake on Cloudflare's part. Both are recoverable by a site small enough to compress its own bytes ahead of time.

## Three limits that turned out innocent

Serving my own pre-compressed bytes from a [Worker][cloudflare-workers] produced brotli inside brotli: a body that decodes once into more compressed bytes, which no client can read. I diagnosed that three times and was wrong three times. Each suspect was a *real* behavior, which is what made them so convincing:

1. **A Worker cannot read the client's [Accept-Encoding][accept-encoding].** True, and worth knowing. Four requests sending `identity`, `br`, `gzip` and `br;q=0` all arrived at the Worker as `"gzip, br"`. The runtime normalizes it, so any `if (acceptsBrotli(request))` branch is dead code that always takes the true arm.
2. **The edge does not down-convert.** Also true. Hand it brotli, ask as a client that cannot decode brotli, and you get raw brotli anyway. So negotiation cannot be faked from behind either.
3. **The static-assets layer re-compresses.** False, and I built a whole parallel `/abr/` path to route around it before finding out.

I finally isolated it by deleting every variable: a 30-byte brotli constant, built in the Worker, touching no assets at all. It came back as 34 bytes in two layers. With no asset fetch in the path, the assets layer was exonerated and the bug had to be mine.

## The actual bug, which was one line long

Cloudflare's `encodeBody: "manual"` tells the runtime a body is already encoded, so leave it alone. It is **write-only** [Response][response] init. There is no getter. So this, which every response on the site passed through, silently discarded it:

```
return new Response(response.body, {
  status:     response.status,
  statusText: response.statusText,
  headers,          // encodeBody is gone, and content-encoding survives
});
```

The runtime then saw a `content-encoding: br` header on a body it believed was raw, and helpfully compressed it to match. Because a security-header wrapper rebuilt every response, `manual` had never worked anywhere on this site, and the symptom looked exactly like a platform that refuses to serve pre-compressed bodies. I had written that conclusion into the repo's own notes before disproving it.

The generalizable rule: **anything that rebuilds a Response must carry `encodeBody` forward.** Since there is no getter, the loss is invisible at the call site, and the damage surfaces one layer downstream wearing someone else's clothes.

## Deltas, and why they ship as dcz

With the emit path fixed, [shared dictionaries][compression-dictionaries] became reachable. Every shell asset is served with `Use-As-Dictionary`, so a returning [Chromium][chromium] visitor sends back the [SHA-256][sha-256] of the copy it already holds, and the build has already computed the diff against it. A CSS change costs 116 bytes instead of 7,615.

[RFC 9842](https://www.rfc-editor.org/rfc/rfc9842.html) defines two encodings for this, and Cloudflare passes both through identically, so the choice is free and purely about engineering. Brotli compresses smaller. zstd decodes much faster:

| measure | dcb (brotli) | dcz (zstd) |
| --- | --- | --- |
| all 12 shipping [deltas][delta-encoding], total | 3,344 bytes | 3,589 bytes |
| widest single gap (nav.js) | 685 bytes | 729 bytes |
| decode, 47,615 B rebuilt | 0.1368 ms | 0.0165 ms |
| throughput | 332 MiB/s | 2,752 MiB/s |

I argued for brotli on bytes and got talked out of it, correctly, and then the numbers I argued from turned out to be wrong in *both* directions. Re-measured against real dictionaries rather than stand-ins, brotli's size edge grew from the one byte I had to 245 bytes across the set (6.8%, winning 11 of 12). zstd's decode edge grew from the 2x I had to 8.3x. The call held anyway, which is the interesting part.

It held for a structural reason: **decode scales with the size of the file it rebuilds.** A 685-byte dcb delta still rebuilds 47,615 bytes, so making the delta smaller never makes the decode gap smaller. Break-even at 9 Mbps is about 135 bytes saved per asset; dcb saves 44, so dcz wins by roughly 3x. On a slow phone the margin widens, because decode scales with the CPU while 44 bytes stays 44 bytes.

Worth saying plainly: on the old numbers this was a coin flip, and on these it's lopsided. The size table now favours brotli by more than it ever did, and that table is the wrong axis to decide on.

dcz's framing is the tidier of the two as a bonus. The dictionary hash rides in a Zstandard *[skippable frame][skippable-frame]*, which is valid zstd, so any conforming decoder steps over it without special handling. dcb needs bespoke treatment of its 36-byte prefix.

Cloudflare runs a live demo of the mechanism at [canicompress.com](https://canicompress.com): a 94KB mock [SPA][single-page-application] bundle redeployed every minute, each version delivered as a delta against the one before. Running its curl recipe from this laptop gave 95,425 bytes raw, 21,702 through [gzip][gzip], and **134 as dcz**. That is 162x under gzip. It lands in the same couple of hundred bytes as a change to this site's far smaller stylesheet, because a delta is priced by what changed rather than by what the file weighs.

The two do the work in different places. Cloudflare's edge holds the dictionary and computes each delta on demand, so the encoder runs per request; here the build computes it once and the Worker only has to pick the right file. The demo lands on dcz too, which is some outside evidence for the call above.

## One more over-generalization

Dictionary compression is unreachable from Node's [zlib][zlib], so the deltas started life as a workstation script with committed artifacts. That sentence was true of brotli. I applied it to zstd without checking, and `zstdCompressSync` takes a `dictionary` option. It also beats shelling out to the CLI: 116 bytes against 120.

So the deltas moved into the build, which removed the CLI dependency, the committed artifacts, a manual step, and the tripwire that existed to catch you forgetting the manual step. Four things deleted because one assumption got tested. The check that mattered was interop: the foreign `zstd -d -D` CLI decodes Node's output byte-exact, prefix included, because the real decoder lives in a browser.

**Update, August 2026:** the brotli half expired too. Node 26 added a `dictionary` option to brotli ([nodejs/node#61763](https://github.com/nodejs/node/pull/61763)), so on v26.7.0 a quality-11 compress of a 21 KB target goes from 70 bytes with no dictionary to 18 with one. The limit I over-generalized was accurate about brotli the day I wrote it and is now accurate about neither encoder. So a runtime limit has a shelf life, and the version you measured on belongs in the note next to the measurement.

## What shipped

| asset | before | every browser now | returning Chromium |
| --- | --- | --- | --- |
| nav.js | 15,909 zstd | 13,047 | delta on change |
| luna.css | 9,124 | 7,615 | 116 |
| lens.js | n/a | 16,491 | delta on change |
| icons.svg | n/a | 2,344 | delta on change |

Roughly 19% off the shell for everyone, and 65x off a CSS change for a returning Chromium visitor. An unplanned side effect: because the Worker now hands over its own brotli, a client offering `zstd, br, gzip` stops getting the zstd that started this whole investigation.

## The part that transfers

Every fact I gathered about the platform was correct. The conclusion I built out of them was wrong. It survived three rounds because each new measurement confirmed a real constraint, just never the one doing the damage. Accumulating true facts feels like converging on a cause.

What broke the loop was a test with none of my code in the path. That is the cheap move I should reach for first. The honest version of "it's a platform limit" is a reproduction the platform owner could run. Until you have that, the call is coming from inside the house.

[cdn]: https://en.wikipedia.org/wiki/Content_delivery_network "A content delivery network: servers spread around the world that keep copies of a site's files close to visitors so pages load faster."
[zstd]: https://en.wikipedia.org/wiki/Zstd "Zstandard, a compression algorithm from Meta built for very fast decompression. Browsers accept it as a way to shrink web pages in transit."
[brotli]: https://en.wikipedia.org/wiki/Brotli "A compression algorithm from Google built for the web. It ships with a built-in dictionary of common web text, which helps on small files."
[cloudflare-workers]: https://developers.cloudflare.com/workers/ "Code that runs on Cloudflare's servers around the world and answers each request directly, with no server of the site's own."
[accept-encoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding "The HTTP request header where a browser lists the compression formats it can decode, such as gzip, br and zstd."
[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response "The JavaScript object a web server or Worker builds to describe its reply: status code, headers and body."
[compression-dictionaries]: https://www.rfc-editor.org/rfc/rfc9842.html "A scheme where the browser keeps an earlier copy of a file and the server sends only a compressed difference against it."
[chromium]: https://en.wikipedia.org/wiki/Chromium_(web_browser) "The open-source browser project that Chrome, Edge, Brave and Opera are built on."
[sha-256]: https://en.wikipedia.org/wiki/SHA-2 "A hash function that turns any input into a 256-bit fingerprint. Change one byte of the input and the fingerprint changes completely."
[delta-encoding]: https://en.wikipedia.org/wiki/Delta_encoding "Storing or sending only the differences between two versions of data, instead of the whole new version."
[skippable-frame]: https://www.rfc-editor.org/rfc/rfc8878.html#name-skippable-frames "A block inside a zstd stream that carries extra data and is marked so every decoder steps over it without reading it."
[single-page-application]: https://en.wikipedia.org/wiki/Single-page_application "A web app that loads one page and then rewrites it with JavaScript as you move around, instead of fetching new pages."
[gzip]: https://en.wikipedia.org/wiki/Gzip "The oldest compression format every browser accepts for web pages. It's fast and universal, and its files come out larger than brotli's or zstd's."
[zlib]: https://en.wikipedia.org/wiki/Zlib "A widely used compression library. Node.js names its built-in compression module after it, and that module also covers brotli and zstd."

Source: https://aadhar.sh/garage/compression
