Zstd dictionary experiment · swdev
EXPERIMENT REPORT · swdev · 2026-11-10
Custom zstd dictionaries on real GitHub events
1,000 untouched evaluation records, one JSON event per S3 object. Separate first 1,000 records for training.
GH Archive: 2025-01-01 00:00 UTC · SeaweedFS level 1 · identical input bytes on both paths.
43.9%smaller than plain zstd
10.5%smaller including both dictionaries
3,000 / 3,000byte-for-byte read checks passed
| Storage representation | Bytes | Relative to original |
| Original JSON | 2,323,484 | 100.0% |
| Plain zstd | 687,542 | 29.6% |
| Zstd + dictionary (object payloads) | 385,946 | 16.6% |
| Dictionary payloads + entire registry | 615,646 | 26.5% |
Measured raw volume payload bytes. Excludes needle/index overhead, filer metadata, replication and filesystem allocation.
Both registered dictionaries total 229,700 bytes; payload break-even is 762 similar objects.
Different event types, different savings
| Event type | Objects | Plain zstd bytes | Dictionary bytes | Saving |
|---|
| CommitCommentEvent | 2 | 2,340 | 1,653 | 29.4% |
| CreateEvent | 100 | 34,015 | 13,998 | 58.8% |
| DeleteEvent | 16 | 5,212 | 2,385 | 54.2% |
| ForkEvent | 5 | 7,288 | 3,603 | 50.6% |
| GollumEvent | 1 | 358 | 213 | 40.5% |
| IssueCommentEvent | 25 | 74,762 | 60,828 | 18.6% |
| IssuesEvent | 5 | 7,157 | 4,707 | 34.2% |
| MemberEvent | 1 | 521 | 263 | 49.5% |
| PublicEvent | 1 | 257 | 110 | 57.2% |
| PullRequestEvent | 43 | 125,875 | 82,595 | 34.4% |
| PullRequestReviewCommentEvent | 1 | 3,334 | 2,314 | 30.6% |
| PullRequestReviewEvent | 4 | 15,802 | 10,739 | 32.0% |
| PushEvent | 743 | 391,929 | 192,329 | 50.9% |
| ReleaseEvent | 5 | 4,986 | 3,499 | 29.8% |
| WatchEvent | 48 | 13,706 | 6,710 | 51.0% |
This is a report generated by the experiment script, separate from the SeaweedFS Admin UI.
Raw results · Per-object CSV
ACTUAL WEED SHELL CAPTURE
Train, compare held-out samples, bind
Only the training records existed in this bucket when the command ran. The final 1,000 evaluation records were still held back.
$ fs.zstd.dict -bucket gh-events-dict -samples 512 -apply
bucket gh-events-dict: sampled 1115429 bytes in 512 samples from 1000 files scanned; skipped 0 encrypted, 0 already compressed, 0 changed while sampling, 0 unreadable
bucket gh-events-dict: trained dict 2365163660 (114850 bytes) on 410 samples, generation 1
bucket gh-events-dict: 102 held-out samples compress to 61709 bytes with no dictionary, 32420 bytes with dict 2365163660 (47.5% smaller)
bucket gh-events-dict .json: trained dict 25209916 (114850 bytes) on 410 samples, generation 1
bucket gh-events-dict .json: 102 held-out samples compress to 61709 bytes with no dictionary, 32420 bytes with dict 25209916 (47.5% smaller)
bucket gh-events-dict bound to /etc/zstd/dicts/2365163660.zdict
bucket gh-events-dict .json bound to /etc/zstd/dicts/25209916.zdict
Published binding
$ fs.zstd.dict -bucket gh-events-dict -show
bucket gh-events-dict: dict /etc/zstd/dicts/2365163660.zdict (generation 1)
bucket gh-events-dict .json: dict /etc/zstd/dicts/25209916.zdict (generation 1)
READ AND CONTENT-TYPE CONTROLS
Same content through S3, including after unbind
1000/1000 plain-zstd S3 GETs match the original archive bytes.
1000/1000 dictionary-zstd S3 GETs match the original archive bytes.
1000/1000 dictionary objects still match after unbinding.
1000/1000 dictionary frames name a nonzero dictionary ID.
1000/1000 plain frames use no dictionary.
application/json with a .json key is compressed with a dictionary.
application/json with a .bin key uses the bucket default dictionary.
Stop using the dictionary for new writes
$ fs.zstd.dict -bucket gh-events-dict -unbind -apply
bucket gh-events-dict unbound (dict 2365163660 remains at /etc/zstd/dicts/2365163660.zdict for existing objects)
Uploads use ContentType='application/json' and .json object keys.
The .json control stores 239 compressed bytes;
the same content under a .bin key uses the default dictionary and stores 239 compressed bytes.