memtrace
PlaygroundBetaLearn memoryFollow on

Getting started

  • Introduction

Unsafe Rust

  • Introduction
  • Raw pointers
  • Undefined behavior

Real world

  • Cloudflare: 100 TB saved in 1.1.1.1's DNS cache

Playground

  • How the Playground works
  • What the Playground can't do yet

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 cache

The 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

Is this unsafe Rust?

No, and that's the point. All five changes are safe Rust. But they come from thinking about exactly what unsafe code has to think about: how big a type is, where its bytes live and what each pointer costs. The one unsafe piece is their benchmark's allocation counter, which implements the unsafe trait GlobalAlloc

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());
5
6 let exact: Box<[u8]> = list.into_boxed_slice();
7 println!("Box<[u8]>: len {}", exact.len());
8}
$Vec: len 3 cap 4 · Box<[u8]>: len 3
STACKone frame per callHEAPallocated at runtimemain()main()runningrunninglist.ptr = 0x2468list.ptr0x2468list.len = 3list.len3list.cap = 4list.cap4Vec<u8> buffer · @ 0x2468123·
Before: three fields on the stack, and a fourth heap slot that is reserved but never used
STACKone frame per callHEAPallocated at runtimemain()main()runningrunningexact.ptr = 0x2468exact.ptr0x2468exact.len = 3exact.len3Box<[u8]> buffer · @ 0x2468123
After: 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 entry

2. 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;
3
4#[allow(dead_code)]
5enum Inline {
6 A(Ipv4Addr),
7 Naptr([u8; 140]),
8}
9
10#[allow(dead_code)]
11enum Boxed {
12 A(Ipv4Addr),
13 Naptr(Box<[u8; 140]>),
14}
15
16fn main() {
17 println!("Inline: {} bytes", size_of::<Inline>());
18 println!("Boxed: {} bytes", size_of::<Boxed>());
19}
$Inline: 141 bytes · Boxed: 16 bytesVisualize in Playground
A small model of the same idea. The rare big variant pays for one extra allocation, and every A record gets smaller

5. 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_of is the first tool to reach for
  • Vec and String are for building; Box<[T]> and Box<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 unsafe only when measuring shows it's needed

Resources

  • Cloudflare Blog: How we saved 100 terabytes of memoryThe original post, with all the measurements
  • std::mem::size_ofHow to check the size of any type
  • The Rust Reference: Type layoutHow size, alignment and padding are decided
  • std::alloc::GlobalAllocThe unsafe trait behind custom and counting allocators
PreviousUndefined behaviorNextHow the Playground works

On this page

  1. The story
  2. 1. Drop the capacity field
  3. 2. One list instead of three
  4. 3. Don't store what you already know
  5. 4. Box the big enum variants
  6. 5. Keep records as raw bytes
  7. Results
  8. What I take from it
  9. Resources