How Cloudflare cut 100TB from the 1.1.1.1 DNS cache
- YT :: https://www.youtube.com/watch?v=dCwWXEI1-lA
- Original title :: Understanding DNS can pay off
A walkthrough of Cloudflare's blog post on shrinking the 1.1.1.1 DNS cache in Rust. The framing number that makes the whole exercise make sense: Cloudflare holds over 250 billion cache entries at any moment, so a single wasted byte per entry costs more than 250 GB of memory across the fleet. Four data-layout changes added up to 100 TB saved.
DNS refresher
You ask for example.com and don't know where it is, so the resolver asks the top-level domain, which doesn't know either but knows which name server has authority. The resolver asks that name server, gets the A record, caches it, and hands the answer back. The next client asking gets the cached answer immediately. That cache is also, Prime notes, why your local networking sometimes breaks for no visible reason until you restart.
Optimisation 1: Vec to boxed slice
A Vec is a pointer, a length and a capacity — 24 bytes of envelope, plus whatever capacity was over-allocated for growth that will never happen. His illustration: three items used out of eight allocated means 48 bytes used against 128 bytes reserved, 80 wasted, plus 8 bytes for the capacity field itself.
Cache entries are never updated once written, so growth capacity is pure waste. Each entry held eight vector and string fields (a String being a Vec<u8> underneath). Replacing them with Box<[T]> and Box<str> saves 8 bytes per field plus 64 bytes per entry, and drops the reserved heap slack. Over 250 billion entries that alone is more than 15 TB.
Optimisation 2: collapse the repeated sections
A DNS message keeps answer, authority and additional record sections, each stored as its own boxed slice — three pointer+length pairs, 48 bytes. Collapsing them into one struct with a single slice of all records plus two offsets marking where authority and additional begin takes it to about 20 bytes for the same information. Struct alignment means it won't land exactly on 20, but it is still terabytes.
Optimisation 3: drop the redundant owner name
A record carries the owner name, class (IN), TTL, type and data. But the cache key already contains the name, so when the record's name matches the key it can be omitted entirely — an Option<Box<str>> that is None in the common case and only populated when the name differs.
Optimisation 4: box the enum payload — the big one
The record data is an enum. IPv4 is 4 bytes, IPv6 is 16, but NAPTR is 136 — and every variant of an enum occupies the size of the largest, so every A record was sitting in 144 bytes. Since A and AAAA are over 80% of traffic, most records were wasting more than 120 bytes on padding. Boxing the payload onto the heap shrinks the enum to a pointer.
The trade-off is real: the payload now lives elsewhere, so cache locality suffers. The blog covers how they clawed that performance back.
Results
- P99 memory per instance: 9.3 GB → 5.3 GB, a 43% reduction.
- P90: 6.5 GB → 3.8 GB, 42%.
- Instances with fuller caches saved more in absolute terms.
- Cache insert throughput: 625,000 → nearly 900,000 entries/second.
- Lookup latency down ~150 ns, 19% faster.
- ~100 TB saved across the fleet.
Takeaways
- Fleet scale turns per-byte layout decisions into capex. The whole post is ordinary struct-packing reasoning; only the multiplier is unusual.
- Immutable data has no business carrying growth capacity —
Box<[T]>overVecwhenever entries are write-once. - Enum size is the size of its largest variant. When the rare variant is 30x the common one, box it.
- Anything already present in the key is redundant in the value.
- Prime's answer to "how does the world's largest DNS cache still use ~Vec~": 1.1.1.1's Big Pineapple was rewritten in Rust fairly recently, so the service is younger than it looks — get off their back.