---
title: "aadhar.sh/garage/dictionary: The dictionary the runtime dropped"
description: "13 pages here ship as plain brotli because workerd's zstd accepted a dictionary option and dropped it. What a fix would save, and what it can't reach."
path: "/garage/dictionary"
section: "garage"
kind: "content"
updated: "2026-09-25"
source: "https://aadhar.sh/garage/dictionary"
---

> 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.

# The dictionary the runtime dropped

Most pages on this site reach a returning [Chromium][chromium] visitor as a [delta][delta-encoding]. The build compresses each one against a 64 KB [dictionary][compression-dictionaries] made from the site's own markup, and a browser that already holds that dictionary downloads only what differs. Measured against production on 2026-09-25, 51 of the 66 pages here answer that way, and 2 more join them with the release rolling out as I write this.

The other 13 answer as plain [brotli][brotli]. They're the pages the [Worker][cloudflare-workers] writes when you ask for them, like [/ledger](https://aadhar.sh/ledger), [/around](https://aadhar.sh/around) and [/finger](https://aadhar.sh/finger), so no build ever sees their bytes.

The Worker could compute the delta itself, since `node:zlib`'s `zstdCompressSync` takes a `dictionary` option. In [workerd][workerd] it accepted that option and threw it away.

I filed that as [cloudflare/workerd#6967](https://github.com/cloudflare/workerd/issues/6967) and wrote the fix, [cloudflare/workerd#7106](https://github.com/cloudflare/workerd/pull/7106), which is waiting on review. This page is what the fix is worth to this site, what it costs, and the part it can't reach. [/garage/compression](https://aadhar.sh/garage/compression) covers the build-time half.

## This page, twice

This page is one of the delta pages. Your browser downloaded it once already, as the document you're reading, and the script below asks for it again with `fetch()`.

A [fetch][fetch-api] has the [destination][request-destination] `empty`, and the site offers its dictionary only for requests whose destination is `document`, so the second copy arrives without the delta. Same bytes, two transfers:

enable JavaScript to run the measurement

A page the Worker writes gets the second number both times. That gap, paid on every visit to those 13 pages, is what the fix is for.

## Why nothing complained

A dropped dictionary fails silently on the compressing side, and [zstd][zstd]'s design is why. A frame compressed without a dictionary is still a valid frame, and it decodes fine when the reader supplies one anyway. So the only symptom is a delta the same size as no delta at all.

The site's nightly canary runs a four-line control for exactly this. It compresses one 21,200-byte buffer three ways: with no dictionary, with the right one, and with a wrong one.

runtime · none · right · wrong

Node 26.9.0 · 73 B · 24 B · 73 B

workerd, as pinned here · 73 B · 73 B · 73 B

A runtime that honours the option prints a smaller number for the right dictionary alone. workerd prints the same number three times. Decompressing is louder: a frame that needs a dictionary fails there as `Data corruption detected`, against bytes that are fine.

The cause was plumbing. The options cross from JavaScript into C++ through a struct that lists the fields it reads. `dictionary` wasn't one of them, so the value vanished at the language boundary before zstd ever saw it. The fix adds the field and hands the bytes to `ZSTD_CCtx_loadDictionary` and `ZSTD_DCtx_loadDictionary`, the way Node does.

## What it would save

For each of the 13 pages, `tools/runtime-dcz-probe.ts` compressed the live HTML at zstd level 6 against the 64 KB dictionary production serves. It then compared each result with the brotli the edge sends today.

Together they drop from 81,228 to 63,449 bytes, 21.9% off. Each bar is today's brotli, and the green end is the part a runtime delta removes:

[/cache](https://aadhar.sh/cache) · 10,333 B · 3,162 → 1,938 B, 1,224 saved (39%)

[/encode](https://aadhar.sh/encode) · 10,408 B · 3,231 → 2,028 B, 1,203 saved (37%)

[/finger](https://aadhar.sh/finger) · 10,637 B · 3,111 → 1,914 B, 1,197 saved (38%)

[/radar](https://aadhar.sh/radar) · 10,669 B · 3,280 → 2,086 B, 1,194 saved (36%)

[/dict](https://aadhar.sh/dict) · 11,194 B · 3,546 → 2,278 B, 1,268 saved (36%)

[/agent-ready](https://aadhar.sh/agent-ready) · 11,625 B · 3,611 → 2,381 B, 1,230 saved (34%)

[/inbox](https://aadhar.sh/inbox) · 14,446 B · 4,316 → 2,943 B, 1,373 saved (32%)

[/lens/census](https://aadhar.sh/lens/census) · 16,395 B · 4,625 → 3,155 B, 1,470 saved (32%)

[/ledger](https://aadhar.sh/ledger) · 20,132 B · 5,541 → 3,910 B, 1,631 saved (29%)

[/around](https://aadhar.sh/around) · 23,996 B · 5,974 → 4,505 B, 1,469 saved (25%)

[/coffee](https://aadhar.sh/coffee) · 33,539 B · 7,711 → 6,533 B, 1,178 saved (15%)

[/serendipity](https://aadhar.sh/serendipity) · 52,539 B · 15,276 → 13,606 B, 1,670 saved (11%)

[/reading](https://aadhar.sh/reading) · 63,199 B · 17,844 → 16,172 B, 1,672 saved (9%)

The percentages spread from 9% to 39%, and the bytes saved barely move. Every page sheds between 1,178 and 1,672 bytes, a median of 1,268, across a sixfold range in page size.

That constant is the shell. The dictionary is built from this site's own pages, so what it matches is the markup they share: the head, the window frame, the tags that load the shell scripts. Every page carries about the same amount of it.

A utility page like [/cache](https://aadhar.sh/cache) is mostly shell and loses 39%, and [/reading](https://aadhar.sh/reading) is mostly its own list and loses 9%.

## What it would cost

A build-time delta costs nothing when a visitor arrives. A runtime delta costs CPU on every request, and this site runs on [Workers Free][workers-free], where a request gets about 10 ms of it.

level 3 · 68,167 B · 16.1% off · median 0.239 ms, max 0.475 ms

level 6 · 63,449 B · 21.9% off · median 0.165 ms, max 0.503 ms

level 19 · 60,237 B · 25.8% off · median 3.451 ms, max 9.904 ms

Those times are Node 26.9 on an M3 Max, calling the same zstd library workerd wraps, because workerd itself can't run the dictionary call until the fix ships. At level 6 the median page took 0.165 ms and the slowest 0.503 ms. Level 19 buys 3.9 more points and spends up to 9.904 ms on /reading, which is the whole allowance, so level 6 is where the curve bends.

More than half of each level-6 call goes to the dictionary. Without it, the same call takes a median 0.072 ms. `node:zlib` has no way to keep a prepared dictionary between calls, so a Worker would load all 64 KB into a fresh context on every request.

## What it can't reach

The homepage fetches two fragments after it loads, the photo grid and the now-playing list. Both are `fetch()` requests, so the dictionary never answers them, for the same reason the demo's second copy arrives whole.

Forced to use it anyway on 2026-09-15, the grid came out at 1,605 bytes at level 6, against the 1,588 bytes of brotli it already ships. A fragment is mostly unique content, with little shell for a dictionary to match.

The fix makes the delta possible and changes nothing here by itself. It still needs a review and a workerd release. The site's wrangler pin picks up a release within a day, and then the canary's control stops printing the same number three times.

After that the Worker needs the dictionary bytes on hand, an [ETag][etag] per encoding, `Vary` on `Available-Dictionary`, and a plain response for any dictionary it doesn't recognise.

## What testing the fix turned up

The PR's first coverage report said 7% of its changed lines ran, which was mostly an artifact: Codecov counted the test file itself as untested code. Closing the real gap took four more tests and turned up three things.

- Node changed the rules while the PR was open. Its zstd functions used to ignore a `dictionary` that wasn't a buffer; since [nodejs/node#65867](https://github.com/nodejs/node/pull/65867), first released in v26.10.0, they throw `ERR_INVALID_ARG_TYPE`. The PR throws too now, and its test passes under v26.10.0 and fails under v26.9.0.
- workerd never checks a wrong `pledgedSrcSize` when it compresses in one call. zstd replaces the promised size with the real one whenever all the input arrives at once. Node catches the mismatch by counting the input itself, and workerd needs the same fix, in a PR of its own.
- A [trained dictionary][zstd-trained-dictionary], the kind `zstd --train` makes, writes its ID into every frame, so a reader without it fails cleanly as `Dictionary mismatch`. This site's dictionaries are raw bytes with no ID, so the frame can't name them. The dcz header's [SHA-256][sha-256] is what tells the browser which dictionary a delta needs.

## The road the site took anyway

While this page was being written, the site moved two of the original 15 off the Worker's per-request path. [/whoareyou](https://aadhar.sh/whoareyou) and [/garage/dyno](https://aadhar.sh/garage/dyno) now bake their frame at build time, so it ships as a delta today, and fetch the parts that change as a fragment ([#924](https://github.com/oddharsh/site/pull/924), [#926](https://github.com/oddharsh/site/pull/926)).

That road needs no workerd release and no CPU per request. It costs a second request and a placeholder that can't shift the layout, and the data arrives as a fragment, which the dictionary can't reach. On /garage/dyno the first visit came out even: 8,158 bytes across two requests, against 8,060 bytes in one.

Most of the remaining 13 could take the same treatment, one page at a time. The runtime fix is the version that needs no second request, no placeholder and no per-page work, which is why it's worth finishing with the other road open.

## Why bother for 1.3 KB

The objection writes itself: 1.3 KB is a rounding error next to a page of photos. For any single request that's fair.

What changes the arithmetic is the dictionary itself. A returning visitor already paid for it, 14,754 bytes the first time, and today it earns nothing on the 13 pages the Worker writes. The fix lets one download work on every page the site serves, whoever wrote it.

watching · the understanding check is part of the page, not a gate

[chromium]: https://en.wikipedia.org/wiki/Chromium_(web_browser) "The open-source browser project that Chrome, Edge, Brave and Opera are built on."
[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."
[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."
[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."
[workerd]: https://github.com/cloudflare/workerd "Cloudflare's open-source runtime that runs Workers code, both in production and on a developer's own machine."
[fetch-api]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API "The browser's JavaScript function for requesting a URL from a script and reading the reply."
[request-destination]: https://developer.mozilla.org/en-US/docs/Web/API/Request/destination "A label the browser attaches to each request saying what it's for, such as document, image or script. Requests made with fetch() leave it empty."
[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."
[workers-free]: https://developers.cloudflare.com/workers/platform/limits/ "Cloudflare's no-cost Workers plan. It allows 100,000 requests a day and 10 ms of CPU time per request."
[etag]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag "An HTTP response header that names one exact version of a file, so a cache can ask whether its copy is still current."
[zstd-trained-dictionary]: https://github.com/facebook/zstd#dictionary-compression-via-training "A zstd dictionary built by zstd --train from sample files. It holds their most common byte patterns and an ID that each compressed frame records."
[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."

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