Article summary — original
Cloudflare Cuts 1.1.1.1 DNS Cache Memory by 100 Terabytes Through Five Structural Optimizations
Cloudflare optimized the in-memory cache of Big Pineapple—the system powering 1.1.1.1 and other DNS services holding over 250 billion entries—slashing per-entry footprint by 56% and reclaiming approximately 100 terabytes of RAM fleetwide while boosting lookup speed.
Big Pineapple stores over 250 billion entries, meaning saving a single byte per entry recovers more than 250 gigabytes across the fleet.
Replacing dynamic Vec and String types with Box<[T]> and Box<str> eliminated capacity overhead and unused heap allocation, saving over 15 terabytes.
Merging record sections, inferring redundant domain owners, and moving large enum variants out-of-line systematically reduced struct padding and allocator waste.
Storing record payloads as raw byte buffers rather than parsed enums restored contiguous memory locality and enabled zero-copy responses for common record types.
Production resident memory dropped by 43% at p99 and 42% at p90, with insert throughput increasing 43% and lookup latency falling 19%.
Eliminating Vector Capacities and Merging Record Lists
DNS cache entries are immutable once written, making the capacity fields in standard Rust Vec and String types redundant. Replacing eight dynamic fields per entry with Box<[T]> and Box<str> removed 64 bytes of struct overhead and eliminated heap overallocation, yielding over 15 terabytes in fleet savings. Additionally, the answer, authority, and additional record sections—previously three distinct heap allocations—were consolidated into a single contiguous list indexed by two-byte u16 offsets, eliminating pointer overhead and reducing alignment padding alongside packed bitflags.
Omitting Redundant Owners and Managing Enum Padding
While DNS wire compression uses two-byte pointers, the cache initially retained full owner names. Because most record owners match the query domain, the cache now omits the owner when identical and reconstructs it from the query key at read time, avoiding heap allocation. Furthermore, Rust enums scale to their largest variant, forcing 4-byte A and 16-byte AAAA records to occupy 144 bytes alongside 136-byte NAPTR records. Boxing the larger, rarely used variants moved their data to separate heap allocations, preventing 120 bytes of wasted padding across the dominant 80% of traffic.
Raw Byte Serialization and Memory Locality
Boxing enum variants created allocator bin rounding waste in jemalloc and degraded CPU cache locality by scattering records across the heap. To resolve this without parsing entire wire-format messages, Big Pineapple adopted a hybrid format: record metadata remains structured, but record data is packed sequentially into a single length-prefixed Box<[u8]> buffer. Because serialized data is assembled in a reusable scratchspace buffer before a single allocation and memcpy, insert throughput grew by 13%. Furthermore, common types like A, AAAA, TXT, and DNSSEC records are copied directly into outgoing wire responses without field-by-field reserialization.
Fleet Rollout and Production Performance Gains
Across a phased deployment between May 18 and July 6, 2026, resident memory at p99 fell from 9.3 GB to 5.3 GB (a 43% reduction) and at p90 dropped from 6.5 GB to 3.8 GB (a 42% reduction). In synthetic benchmarks matching production distributions, per-entry footprint fell from 953 bytes to 420 bytes, cache insert throughput rose from 625,000 to 893,000 entries per second, and lookup latency dropped from 828 ns to 670 ns. Cloudflare plans to reinvest the roughly 100 terabytes of freed memory into expanding cache capacity to improve hit rates and reduce upstream query volumes.
Saved original article
[Big Pineapple](https://blog.cloudflare.com/big-pineapple-intro/), the platform behind [1.1.1.1](https://www.cloudflare.com/learning/dns/what-is-1.1.1.1/), [Gateway DNS](https://www.cloudflare.com/products/gateway/), [DNS Firewall](https://www.cloudflare.com/dns/dns-firewall/), [AS112](https://blog.cloudflare.com/the-as112-project/), and several other Cloudflare DNS services, stores over 250 billion DNS cache entries at any given time. At that scale, wasting a single byte per entry costs more than 250 gigabytes of memory across our fleet.
Five successive changes to how cache entries are stored in memory cut the per-entry footprint by over 50%. Across our fleet, these changes freed up roughly 100 terabytes of memory, equivalent to the amount of RAM in 130 of our [Gen 13 servers](https://blog.cloudflare.com/gen13-config/). The cache also got faster. Insert throughput rose 43% and lookup latency dropped 19%, as fewer allocations and better memory locality meant we did not trade speed for space.
On cold start, Big Pineapple starts out with an empty cache. As DNS queries arrive, the cache fills until it hits its maximum entry count, at which point we evict older or less popular items to make room.
The exact cache size varies by data center. When [EDNS Client Subnet (ECS)](https://en.wikipedia.org/wiki/EDNS_Client_Subnet) is in use, authoritative servers return different answers depending on the client's network, so we cache multiple versions of the same query. This increases both the number of entries and the memory each one consumes, making the optimizations in this post especially impactful for ECS-heavy locations.
Each item in the cache is a key-value pair. The key identifies what was queried:
The value stores the DNS response itself: the answer, authority, and additional record sections, along with metadata like the creation time, a hit counter, and the Time-to-Live (TTL).
Both structs have room for improvement. Several fields use types that carry overhead we don't need once the entry is stored.
To measure the impact of each change, we benchmark by filling the cache with randomly generated entries that roughly match the traffic distribution we see in production: 56% `A` records, 25% `AAAA`, and 19% `TXT`. Each entry contains between one and four records.
`TXT` records serve as a stand-in for all non-`A`/`AAAA` record types in the benchmark. Their size is randomized between 64 and 224 bytes, close to the average response size we see for variable-length record types.
We track memory usage using a [custom allocator](https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html) that wraps Rust’s `System` [allocator](https://doc.rust-lang.org/std/alloc/struct.System.html) and records the number and size of allocations per cache entry. Alongside memory, we measure insert throughput and lookup latency across the full cache flow to make sure memory savings don’t come at the cost of performance.
These inputs approximate production rather than reproduce it exactly. Process memory also depends on traffic mix, cache occupancy, allocator state, and memory used outside the cache. We therefore measured resident memory across production instances during the rollout.
`Vec<T>` stores three fields: a pointer to heap-allocated data, the current length, and the total capacity. When you push an item, `Vec` checks whether the length exceeds the capacity and reallocates if needed. If there’s room, it just appends the item and increments the length.
Once we store a DNS response in the cache, however, we never modify it again. The capacity field serves no purpose, but still costs 8 bytes per `Vec`. The over-allocated heap space is wasted as well, as a `Vec` with capacity for eight items but only five stored leaves three slots unused on the heap.
Using `Box<[T]>` solves both problems. It can’t grow after creation, so it doesn’t need a capacity field or reserve space for future elements. The same applies to `String`, which also carries a capacity field. `Box<str>` drops it.
Each cache entry stores 8 `Vec` and `String` fields. Replacing them with `Box<[T]>` and `Box<str>` saves 8 bytes per field, 64 bytes per entry. It also eliminates the excess heap memory that `Vec` reserves for future growth. The combined savings add up to over 15 terabytes with over 250 billion cache entries.
Rather than storing the answer, authority, and additional sections in separate lists, we can store a single list with offsets to the start of each section. Since DNS record counts per section fit in a `u16`, we can use a `u16` (2 bytes) for each offset, compared to the 8-byte pointer and 8-byte length that each separate `Box<[T]>` requires.
This removes two lists, each with an 8-byte pointer and 8-byte length, and replaces them with two 2-byte offsets, saving 28 bytes per entry.
These savings do not always map directly to the number of bytes removed from individual fields. Rust inserts [padding to satisfy alignment requirements](https://doc.rust-lang.org/nomicon/repr-rust.html) and rounds a struct’s size up to a multiple of its alignment. Removing a small field can therefore eliminate additional padding. For example, we also packed several boolean fields into a single [bitflag](https://docs.rs/bitflags/latest/bitflags/). This reduced the surrounding padding, causing the struct to shrink by more than the size of the individual booleans.
Each DNS record has an owner, the domain the record belongs to. In many cases, this owner is identical to the domain being queried. For example, a query for `example.com A` returns two records with the same owner:
But when a `CNAME` is involved, for example, the record owner can differ from the queried domain:
The DNS wire format handles repeated owners using name compression, as defined in [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4). Rather than encoding the same domain twice, subsequent occurrences store a 2-byte pointer to the first occurrence. A domain like `www.example.com` can encode just `www` followed by a pointer to where `example.com` already appeared in the message.
This works well on the wire, but in our cache we store the full owner name alongside each record. Following compression pointers during cache lookups is expensive on the hot path, so we trade memory for speed.
Most records, however, have an owner identical to the queried domain. For those, we can drop the owner entirely and infer it at read time. When the owner differs, such as the `A` records behind a `CNAME`, we store the full name.
When `owner` is `None`, response construction restores the queried domain from the cache key, avoiding a heap allocation. This means the record is no longer self-contained, but the cache key is already available during every lookup. When the owner differs, `Some` stores a pointer to the full name on the heap.
In practice, most cached records have an owner identical to the queried domain, so the majority require no heap allocation for the owner field.
Rust enums are [sum types](https://en.wikipedia.org/wiki/Algebraic_data_type): each variant can carry different data, but the enum is always the size of its largest variant.
`Option` is either `Some` and holds a value, or `None` and holds nothing. Both variants take the same amount of memory. The enum stores a tag indicating the active variant, followed by space large enough for the largest variant’s data. When the variant is `None`, that space is unused.
For record data, it seems natural to store each DNS record type as an enum variant:
But the enum is always as large as its largest variant. In our case, that’s `NAPTR` at 136 bytes. It stores three variable-length text fields, a domain name, and two integers. As a result, the full enum, including the variant tag and padding, becomes 144 bytes.
An `A` record only needs 4 bytes, and an `AAAA` record needs 16 bytes. `A` and `AAAA` make up over 80% of our traffic, so most records waste over 120 bytes on padding. Since a single cache entry can store many records this quickly adds up.
To solve this problem, we can box the larger variants of the enum, moving them to a separate heap allocation. The enum then stores an 8-byte pointer to the heap, where the data takes up only the size it actually requires.
For `A` and `AAAA` records, this saves 120 bytes per record. Smaller variant types like `TXT` and `CNAME` also benefit. They still occupy the 24-byte enum, but their heap allocation is sized to their actual data rather than padded to 144 bytes. `NAPTR`, the largest variant, actually pays slightly more. It now adds the cost of a heap pointer and allocation overhead. But `NAPTR` records are rare in practice, so the tradeoff is worth it.
But boxing the larger record variants introduces costs of its own.
Boxing has two costs. The first is allocator overhead. Each boxed variant becomes a separate heap allocation, and allocators round up to the nearest size class. Big Pineapple uses [jemalloc](https://jemalloc.net/), an allocator designed for multithreaded, allocation-heavy workloads. jemalloc groups allocations of similar sizes into fixed-size bins. A `TXT` record requests 32 bytes and fits exactly into a 32-byte bin, wasting nothing, but an `MX` record requests 40 bytes and rounds up to 48, wasting 8 bytes.
The second cost is poor memory locality. Without boxing, the record enum values for a cache entry sit in a single contiguous allocation. With boxing, data for each boxed variant lives in a separate heap region. Reading it requires following a pointer, and when that pointer lands far from the rest of the entry, the CPU has to fetch a new cache line. With millions of cache entries, boxed data ends up scattered across the heap rather than packed together.
Neither cost is catastrophic on its own, but eliminating both, as the next section shows, yields a measurable improvement in both memory usage and lookup latency.
An obvious next step would be to store the full DNS response in wire format, patching only per-client fields like the message ID on each lookup. But this has drawbacks. DNSSEC records are only included when the client sets the [DO (DNSSEC OK)](https://www.cloudflare.com/learning/dns/dnssec/how-dnssec-works/) flag. Storing a complete wire format message means either caching two variants, one with DNSSEC and one without, or filtering them out of an already-built message. There is also a cost to parsing the full message on every lookup, which the enum approach we just described avoids by storing already-parsed records.
As a middle ground, we store just the record data as raw bytes, while keeping the rest of the cache entry as structured fields. Instead of a list of parsed enum variants, we store the records as a single `Box<[u8]>` containing each record encoded as a 2-byte length prefix followed by its raw bytes.
This eliminates the per-variant enum overhead and the boxed heap allocations from the previous optimization. The data also becomes packed contiguously, which improves CPU cache locality. The tradeoff is that records can no longer be randomly indexed. We have to iterate through the buffer sequentially. This adds some complexity for features like [round-robin](https://www.cloudflare.com/learning/dns/glossary/round-robin-dns/) rotation of `A`/`AAAA` records, but since record counts per entry are small, the cost is negligible.
When building a DNS response from cached records, most record types can be copied directly from the buffer into the outgoing message. Previously, each parsed record had to be serialized field by field back into DNS wire format. The new layout skips that work for `A`, `AAAA`, `TXT`, and all DNSSEC record types by copying their encoded bytes directly. Only records containing domain names, such as `CNAME`, `NS`, `MX`, and `SOA`, still require parsing so we can apply DNS name compression. Since records that support direct copying make up the vast majority of our traffic, this change reduces work on the lookup path. Combined with improved memory locality, this reduced cache lookup latency by 5% in our benchmarks.
To build the record data buffer, we write into a reusable scratchspace buffer that persists across cache insertions. Since previous writes have already grown it, the buffer rarely needs to be reallocated. Records vary in size, so we do not know the exact buffer size until they have been serialized. Once the records are in the scratchspace buffer, we allocate a `Box<[u8]>` and `memcpy` the data into it. This replaces the separate allocation for each boxed record with one allocation for all record data. It also avoids the waste from shrinking a `Vec<u8>`, where the allocator may not be able to reclaim the unused tail of the original allocation. In our benchmark, this change alone increased cache insert throughput by 13%.
The production measurements show how the benchmarked per-entry savings translated to whole-process resident memory. The graph below shows p90, p98, and p99 memory usage across Big Pineapple instances. The first dashed line marks the start of the rollout on May 18, 2026, and the second marks its completion across all services on July 6, 2026. Each release introduced one or more of the optimizations described above, so memory usage dropped in steps rather than all at once.
As each release rolled out, restarted instances began with empty caches and consumed more memory as those caches filled. The stable plateaus therefore represent steady-state memory usage better than the initial dips.
Per-instance memory usage dropped across all percentiles. At p99, memory dropped from 9.3 GB to 5.3 GB, a 43% reduction in resident memory. At p90, memory dropped from 6.5 GB to 3.8 GB, a 42% reduction. Instances with fuller caches saw the largest absolute savings.
In our benchmarks, these five optimizations reduced the per-entry memory footprint from 953 bytes to 420 bytes, a 56% reduction. Per-entry allocations dropped from 1.1 KB to 461 bytes. The reductions measured in production are smaller because resident memory includes the cache alongside all other process data. After the rollouts settled, aggregate working-set memory across the fleet was roughly 100 terabytes lower.
Performance also improved. Cache insert throughput increased by 43%, while lookup latency dropped by 19%.
| Metric | Before | After | Change |
| Per-entry net footprint | 953 bytes | 420 bytes | -56% |
| Per-entry allocations | 1.1 KB | 461 bytes | -58% |
| Cache insert throughput | 625,000 entries/s | 893,000 entries/s | +43% |
| Cache lookup latency | 828 ns | 670 ns | -19% |
We plan to reinvest the freed memory into increasing cache capacity without increasing our memory usage, which improves cache hit rates and reduces upstream query volume. We're also exploring further optimizations to the cache itself.
To learn more about Big Pineapple, see [How Rust and Wasm power Cloudflare's 1.1.1.1](https://blog.cloudflare.com/big-pineapple-intro/). If you work on DNS or other large systems, share the optimizations that have worked for you in the [Cloudflare Community](https://community.cloudflare.com/) or on the [Cloudflare Developers Discord](https://discord.cloudflare.com/).