Real world
Cloudflare: 100 TB saved in 1.1.1.1's DNS cache
How shrinking one Rust struct, a few bytes at a time, freed about 100 TB of memory across a fleet
Original post · Cloudflare Blog · August 2026How we saved 100 terabytes of memory by optimizing 1.1.1.1's DNS cacheThe story
Cloudflare's DNS resolver keeps more than 250 billion cache entries in memory at once. At that scale every byte counts: one wasted byte per entry is over 250 GB across the fleet
Each entry took about 953 bytes. Five changes to how the entry was laid out brought that down to 420 bytes, 56% less, and the fleet's memory use dropped by about 100 TB
1. Drop the capacity field
A Vec is three words on the stack: pointer, length and capacity. Once a list is built it never grows again, so the capacity is dead weight, and the heap buffer may still have unused slots. Box<[T]> and Box<str> keep only the pointer and length
1fn main() {2 let mut list = Vec::with_capacity(4);3 list.extend([1u8, 2, 3]);4 println!("Vec: len {} cap {}", list.len(), list.capacity());56 let exact: Box<[u8]> = list.into_boxed_slice();7 println!("Box<[u8]>: len {}", exact.len());8}into_boxed_slice trims the buffer to fit, and only a pointer and a length remain. Cloudflare had 8 such fields, so this saved 64 bytes per entry2. One list instead of three
A DNS answer has three sections of records. Three separate lists cost three pointer-and-length pairs. Cloudflare stored all records in one list and kept where each section starts as small u16 offsets, saving 28 bytes per entry. Packing a few bool fields into one set of bit flags removed padding too
3. Don't store what you already know
Most records are about the exact name that was looked up, and that name is already in the cache key. So the record's owner name became Option<Box<Name>>: None in the common case, filled back in from the key when the entry is read
That None costs nothing extra. A Box can never be null, so Rust uses the null value to mean None, and Option<Box<T>> is the same 8 bytes as a plain pointer
4. Box the big enum variants
An enum is as big as its largest variant. A rare record type made every record 144 bytes, even the common A records that only hold a 4-byte IPv4 address. Moving the big variants behind a Box shrank the enum to 24 bytes and saved 120 bytes per A or AAAA record
1use std::mem::size_of;2use std::net::Ipv4Addr;34#[allow(dead_code)]5enum Inline {6 A(Ipv4Addr),7 Naptr([u8; 140]),8}910#[allow(dead_code)]11enum Boxed {12 A(Ipv4Addr),13 Naptr(Box<[u8; 140]>),14}1516fn main() {17 println!("Inline: {} bytes", size_of::<Inline>());18 println!("Boxed: {} bytes", size_of::<Boxed>());19}A record gets smaller5. Keep records as raw bytes
Records used to be parsed into enum values and turned back into bytes on every lookup. Now each entry keeps its records in one Box<[u8]> in DNS wire format, each record prefixed by a 2-byte length. That is one allocation per entry instead of many, and most records are copied into the response as they are
Results
- Memory per entry: 953 → 420 bytes (−56%)
- Insert throughput: 625,000 → 893,000 per second (+43%)
- Lookup latency: 828 → 670 ns (−19%)
- p99 resident memory per server: 9.3 → 5.3 GB (−43%)
- About 100 TB less memory across the fleet, roughly the RAM of 130 servers
What I take from it
- Know the size of your types.
std::mem::size_ofis the first tool to reach for VecandStringare for building;Box<[T]>andBox<str>are for keeping- An enum costs its biggest variant, so box the rare large ones
- Smaller data is often faster too: fewer allocations and better cache use
- Safe Rust already gives a lot of control over layout. Reach for
unsafeonly when measuring shows it's needed