One byte per cache entry sounds like nothing. Multiply it by the 250 billion entries sitting in Cloudflare's DNS caches at any moment, and that "nothing" is over 250 GB of memory. Wasted bytes add up fast when you run at that scale. That is why Cloudflare's engineers spent this spring shrinking the per-entry footprint of the cache behind 1.1.1.1. 1
The result, published this week on the Cloudflare engineering blog: five changes to how cache entries are laid out in memory cut the average entry from 953 bytes to 420 bytes, a 56% reduction, and freed roughly 100 terabytes of RAM across the fleet. That is about what 130 of Cloudflare's Gen 13 servers pack. 12
None of it came at the cost of speed. Cache insert throughput went up 43%, and lookup latency dropped 19%. 1 Most memory-optimization stories end with a tradeoff. This one ends with the cache getting faster, because fewer and better-placed allocations improved memory locality.
The cache behind 1.1.1.1
The cache lives inside Big Pineapple, the Rust-based DNS platform that replaced Knot Resolver as the engine of 1.1.1.1, Gateway DNS, DNS Firewall, and AS112. It holds key-value pairs: the query (qname, qtype, and friends) mapped to the parsed DNS response, plus metadata like TTL and a hit counter. 13
A recursive resolver lives or dies by its cache. A hit answers in under a millisecond; a miss can take hundreds. 3 So every byte the cache wastes is memory that could instead hold more entries, raise the hit rate, and cut upstream query volume. Here is how Cloudflare clawed 533 bytes back, one layout decision at a time.
1. Drop the capacity: Vec to Box<[T]>
The Vec<T> workhorse stores a pointer, a length, and a capacity. That capacity field exists so the vec can grow. But once a DNS response is in the cache, nobody ever mutates it again. The capacity is pure overhead: 8 bytes per field, plus whatever slack the allocator reserved for future growth.
Each cache entry carries 8 Vec and String fields. Swapping them for Box<[T]> and Box<str> (which cannot grow, so they need no capacity) saved 8 bytes per field, 64 per entry. Combined with the heap slack shed from unused capacity, that works out to over 15 TB across 250 billion entries. 1
2. One list, two offsets
The answer, authority, and additional sections were stored as three separate lists, each with its own pointer and length. Cloudflare merged them into a single list, with 2-byte offsets marking where each section starts. DNS record counts per section fit in a u16, so two offsets replace two (pointer, length) pairs: 28 bytes saved per entry. They also packed several booleans into a single bitflag, which trimmed struct padding. Rust rounds a struct's size up to its alignment, so removing small fields can shrink the whole thing more than the fields themselves. 1
3. Drop the owner when it is redundant
Every DNS record carries an owner, the name it belongs to. For a query like example.com A, each answer record's owner is just example.com again, identical to what was queried, yet stored in full next to every record.
Cloudflare changed Record so the owner is Option<Box<Name>>: None when it matches the queried domain (restored from the cache key at read time, no heap allocation), Some(...) only when it differs, like the A records hiding behind a CNAME. Most cached records take the None path, so most stop allocating for the owner entirely. 1
4. Box the fat enum variants
RecordData is a Rust enum with one variant per DNS record type, A(Ipv4Addr), Aaaa(Ipv6Addr), Naptr(...), and so on. An enum is always the size of its largest variant, and here that is NAPTR at 136 bytes, pushing the whole enum to 144. An A record needs 4 bytes; an AAAA needs 16. A and AAAA together are over 80% of traffic, so most records carried about 120 bytes of padding. 1
pub enum RecordData {
// Small and common variants are stored inline
A(Ipv4Addr),
Aaaa(Ipv6Addr),
// Large variants are stored on the heap
Txt(Box<Txt>),
Naptr(Box<Naptr>),
Svcb(Box<Svcb>),
// ...
}pub enum RecordData {
// Small and common variants are stored inline
A(Ipv4Addr),
Aaaa(Ipv6Addr),
// Large variants are stored on the heap
Txt(Box<Txt>),
Naptr(Box<Naptr>),
Svcb(Box<Svcb>),
// ...
}Boxing the large, rare variants moves them to the heap; the enum itself stays small. That saves 120 bytes per A/AAAA record. The rare NAPTR actually pays a little more (a pointer plus allocation overhead), but it is rare enough that the trade is a clear win. 1
5. Store records in wire format
The next step sounds obvious: cache the whole DNS response as raw bytes. It does not quite work. DNSSEC records are only returned when the client asks for them, so a single wire-formatted message cannot serve both kinds of clients, and you would have to parse the whole message on every lookup.
Cloudflare's middle ground: keep the rest of the entry structured, but store the records themselves as one Box<[u8]>, each record encoded as a 2-byte length prefix followed by its raw bytes. That kills the per-variant enum overhead and the boxed allocations at once, and packs all record data contiguously, which the CPU cache likes. Building a response is now mostly a memcpy. Only records with domain names (CNAME, NS, MX, SOA) still need parsing, so Cloudflare can apply DNS name compression. This change alone cut lookup latency 5% in benchmarks and raised insert throughput 13%. 1
The scoreboard
- Per-entry footprint: 953 B to 420 B, down 56%
- Allocations per entry: 1.1 KB to 461 B, down 58%
- Insert throughput: 625,000 to 893,000 entries/s, up 43%
- Lookup latency: 828 ns to 670 ns, down 19%
Production numbers, measured across the rollout that ran May 18 to July 6, 2026, were smaller but still striking: p99 per-instance memory fell from 9.3 GB to 5.3 GB, down 43%; p90 from 6.5 GB to 3.8 GB, down 42%. The freed memory is earmarked for growing cache capacity, which should raise hit rates without raising the memory bill. 1
What to steal from this
Even if you never run a DNS resolver, five of these ideas transfer directly to any Rust codebase that stores lots of immutable records:
Box<[T]>is for data that never changes after construction. You shed the capacity field and everything the allocator slacks off.- Store hot data compactly and look up sections by offset instead of keeping parallel lists.
- Do not store fields you can re-derive. If the owner equals the query, you do not need one per record.
- Box the rare, fat enum variants so common ones never pay for the worst case.
- Keep hot data contiguous. The bytes-and-offsets layout beat parsed structs on latency, not just memory.
The full write-up is worth reading end to end. It covers the benchmark methodology (56% A, 25% AAAA, 19% TXT records, measured with a custom allocator) and the reasoning against full wire-format caching. 1
- How we saved 100 terabytes of memory by optimizing 1.1.1.1's DNS cache (Cloudflare Blog)
- Hacker News discussion ("Saving 100 terabytes of memory by optimizing 1.1.1.1's DNS cache")
- How Rust and Wasm power Cloudflare's 1.1.1.1 (Cloudflare Blog)