The page is 7.7 KB. The stylesheet it loads is 312 KB. Both came from the same host, one second apart, with the same request headers.
The HTML arrived as zstd. The CSS arrived as gzip — even though the same Accept-Encoding header listed brotli, and the same edge was willing to send brotli for the HTML. Asking for brotli alone on that stylesheet returns 33 KB.
So a browser that offers every encoding the server supports downloads 312 KB where 33 KB was available. That is one file, on one of 114 sites I run. I spent an afternoon measuring where else this happens.
What I actually measured
Two datasets, both taken today:
- 684 HTML responses — 114 sites × 6
Accept-Encodingvariants. - 492 assets — the same-origin CSS and JavaScript referenced by those 114 homepages, each fetched three ways (browser header, brotli only, identity).
The whole method is one idea: read the bytes, do not trust the label. No decompression library, no requests magic, no CDN analytics. len(body) after reading the raw stream is the number the user pays for.
import ssl, urllib.request CTX = ssl.create_default_context() CTX.check_hostname = False CTX.verify_mode = ssl.CERT_NONE UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") def wire_bytes(url, accept_encoding): req = urllib.request.Request(url, headers={ "User-Agent": UA, "Accept": "*/*", "Accept-Encoding": accept_encoding, }) with urllib.request.urlopen(req, timeout=30, context=CTX) as r: body = r.read() return { "encoding": r.headers.get("content-encoding"), "wire": len(body), "vary": r.headers.get("vary"), "cache": r.headers.get("cf-cache-status"), } Three details matter more than they look. read() with no argument, because a truncated read makes big files look identical. No brotli library installed, because I do not need to decode anything. And a browser User-Agent, because some of these hosts behave differently for unknown clients.
The HTML got zstd, all 114 of them
Chrome's real header is gzip, deflate, br, zstd. Fonts and assets offered the same set. For HTML, all 114 sites answered zstd:
Accept-Encoding sent | Encoding returned | Sites |
|---|---|---|
gzip, deflate, br, zstd | zstd | 114 / 114 |
gzip, deflate, br | br | 114 / 114 |
br | br | 114 / 114 |
gzip | gzip | 114 / 114 |
identity | none (raw) | 114 / 114 |
br;q=0, gzip;q=0, deflate;q=0 | none (raw) | 114 / 114 |
Two things to like here. identity is honoured everywhere, so the servers are not compression bullies. And refusing every coding with q=0 returns a plain 200 with raw bytes rather than a 406, which is the forgiving behaviour you want from a CDN.
The part I did not expect: zstd loses on size. I fetched every homepage three times and compared the wire bytes per encoding.
| Encoding | Median bytes | Median share of raw | Total across 114 pages |
|---|---|---|---|
raw (identity) | 75,220 | 100% | 9.70 MB |
| gzip | 10,961 | 16.4% | — |
| br | 10,855 | 15.6% | 1.30 MB |
| zstd | 11,710 | 16.4% | 1.37 MB |
Brotli beat zstd on 113 of 114 homepages. Median difference: 566 bytes, 6.3%. Across all 114 pages, zstd cost 72,493 bytes more than brotli would have — 636 bytes per page. Small, but it is a real tax paid for choosing the newer algorithm: at this compression level, the newest codec is the loosest one.
The exception is worth keeping: one homepage ships a 666,877-byte Next.js document with a lot of repeated JSON, and there zstd won by 5.8% (45,740 vs 48,554 bytes). Repetition-heavy payloads are exactly where zstd's window pays off. Brotli is not universally better; it is better on prose-shaped HTML.
The assets got gzip, 480 out of 492
This is where the numbers stop being a rounding error.
I pulled every same-origin stylesheet and script out of those 114 homepages, deduplicated by URL, and fetched each one three times. 480 of 492 came back gzip when the client offered brotli and zstd. Twelve came back zstd. Zero came back brotli.
| Asset type | Files | Raw | Delivered to Chrome | With brotli | Saved |
|---|---|---|---|---|---|
| CSS | 156 | 15.19 MB | 3.58 MB | 2.00 MB | 1.58 MB |
| JavaScript | 335 | 45.07 MB | 13.16 MB | 13.10 MB | 0.06 MB |
| Total | 492 | 60.31 MB | 16.75 MB | 15.11 MB | 1.64 MB |
Nine point eight percent of the bytes every visitor downloads, available for free, not taken.
The distribution is lopsided. Eight CSS files account for 80% of the entire saving, and almost all of it sits in one filename that recurs across the cluster:
$ curl -s -D - -o /dev/null -H 'Accept-Encoding: gzip, deflate, br, zstd' \ https://spacexstockprediction.com/static/css/css2.css | grep -i content-encoding content-encoding: gzip # 312,302 bytes $ curl -s -D - -o /dev/null -H 'Accept-Encoding: br' \ https://spacexstockprediction.com/static/css/css2.css | grep -i content-encoding content-encoding: br # 33,328 bytes Same URL, same cache entry, same minute. The difference is 279 KB per page load on that host, and the same pattern repeats on 15 sites in the cluster.
What is inside a 1.09 MB stylesheet
I fetched it with identity to see what was being compressed so poorly. It is 1,094,451 characters of @font-face rules for Noto Sans SC:
@font-face blocks : 1,010 unicode-range : 1,010 url() references : 1,010 (all self-hosted /static/font/*.woff2) font-display : swap This is the Google Fonts CJK slicing pattern, copied in-house: a hundred-odd unicode-range subsets per weight, several weights. The page that loads it, a SpaceX explainer on the worst offender, contains 223 Han characters. The browser will only fetch the few subsets it needs for those 223 characters, so the fonts themselves are not the problem — the problem is that 1.09 MB of rules has to cross the wire and be parsed before the browser knows which subsets exist.
And this cluster has a language mismatch that makes it worse: there are Chinese-language guides served from an English-language template, which is exactly the setup that ends up shipping a full CJK font manifest to every visitor.
Cheap fix: that file does not change between deploys. Compress it once with brotli at build time, serve the .br variant with Content-Encoding: br and Vary: Accept-Encoding. 1.09 MB becomes 33 KB for every browser released this decade.
Where brotli does not help at all
I expected the JavaScript column to look like the CSS column. It does not:
- brotli beat gzip on 138 of 335 JavaScript files.
- On 196 of 335 it was larger. Median difference: −0.4%.
- Total: 13.16 MB → 13.10 MB. Half a percent.
Whatever level this edge compresses at, it is not the level where brotli's context modelling starts to matter on minified JS. So the honest summary is not "turn on brotli". It is "turn on brotli where the file is repetitive text, and measure the rest" — CSS yes, minified JS no, HTML marginally.
If you want a second opinion on any of this, pick a page from the same template, for example a market page from the same cluster, and compare its content-encoding against curl -H 'Accept-Encoding: br'.
The negotiation table that explains it
Probing one cached CSS file with different header combinations, same URL, all cf-cache-status: HIT:
| Header | Returned | Bytes |
|---|---|---|
gzip, deflate, br, zstd | gzip | 250,677 |
br, gzip | gzip | 250,677 |
gzip, br | gzip | 250,677 |
gzip, deflate, br | gzip | 250,677 |
zstd, br | zstd | 35,999 |
br | br | 33,163 |
It is not header order. Any header that mentions gzip gets gzip on the asset path; drop gzip and the same file is served as zstd; ask for brotli alone and you get brotli. Meanwhile the HTML path, on the same zone, prefers zstd and never falls back to gzip while brotli is on the table.
I cannot see the CDN configuration from outside, so I am not going to claim a cause. What I can say is that the asset path and the document path negotiate differently, in the same zone, in the same second, and that the asset path's choice is the expensive one for CSS.
Two hygiene findings, one good, one not
All 492 assets vary their bytes by Accept-Encoding, and none of them send Vary. All 114 HTML responses do. If anything between the edge and the client caches by URL alone — a corporate proxy, a service worker with a careless cache key, a reverse proxy you forgot about — it can hand brotli bytes to a client that never advertised brotli, and that client will render garbage. The edge's own cache key handles it; nothing downstream is told to.
The other one is smaller but universal: 114 of 114 responses have no Content-Length and use Transfer-Encoding: chunked. Every homepage in the cluster streams without a declared size. That is not a bug, but it removes the browser's ability to show progress or reserve space, and it makes byte-count auditing harder than it should be.
What I am doing about it
- Precompressing the CJK stylesheets at build time and serving brotli variants with
Vary: Accept-Encoding. That is 1.58 MB of the 1.64 MB, and it needs no configuration change on the edge. - Re-checking per type instead of globally. CSS gains 44% of its bytes; minified JS gains 0.5%.
- Adding wire-byte assertions to the deploy check, comparing
len(body)forgzipversusbron the three largest text files. A regression here is invisible in every dashboard I have.
The uncomfortable part is that this cluster has been measuring performance for weeks — TTFB, cf-cache-status, 304 rates, image weight — and every one of those measurements looked fine, because none of them asked what encoding the bytes arrived in. The page was fast. It was also 279 KB heavier than it needed to be, on a file that has not changed since the last deploy.
