August 27, Cloudflare published an engineering report on how its team reduced the size of a single DNS cache entry 1.1.1.1 — from 953 to 420 bytes. Across the server fleet, this freed about 100 terabytes of RAM. Not a single new feature appeared in the resolver: five edits affected only how the data are laid out in memory.
Reading this as “news about Cloudflare” is boring and useless. More interesting: your Unbound on a VPS with a gigabyte of memory runs the exact same arithmetic — just three to four orders of magnitude smaller. And unlike Cloudflare, you set your cache budget by hand — with a number that almost no one has checked since installation.
Who it helps: keep your recursive resolver (Unbound, dnsmasq, AdGuard Home, CoreDNS) in good shape, especially on a machine with little memory. In the end — how to see how much your cache is actually using, and what the numbers in the output mean.
Where the 100 terabytes come from
The Big Pineapple platform, on which 1.1.1.1, Gateway DNS, DNS Firewall and AS112 operate, according to Cloudflare keeps in cache over 250 billion records simultaneously. Hence the main point of the report: an extra byte per record costs more than 250 GB across the fleet. Not “roughly,” but literally — this is simple multiplication.
The memory savings per record came out as follows:
| Metric | Was | Now | Difference |
|---|---|---|---|
| Memory per record | 953 B | 420 B | −56 % |
| Allocations per record | 1.1 KB | 461 B | −58 % |
| Resident memory, p99 | 9.3 GB | 5.3 GB | −43 % |
| Resident memory, p90 | 6.5 GB | 3.8 GB | −42 % |
Note at once a mismatch that can be mistaken for an error: 533 saved bytes on 250 billion records is more than 130 TB, while the reported figure is about 100 TB. There is no contradiction here — the stated figure is the measured difference in resident memory of processes, not the product of multiplication on paper. The upper bound of arithmetic and the actual gain do not have to match.
What was stored in the entry and why it weighs so much
The cache key (CacheKey) consisted of four fields: qname, qtype, authenticated, tag. The value (CacheEntry) — eight: timestamp, inception, ttl, hits, answers, authority, additional, errors. Nothing superfluous in terms of logic. The superfluous part was in the layout.
Three sources of cost are visible to the naked eye. First: record lists were stored in Vec, and Vec carries a capacity field — eight bytes per entry, even when the entry is already built and no longer changes. Second: answer sections answer, authority and additional were stored as three separate lists, i.e., three pairs of “pointer + length.” Third and most telling: the enum of record types was aligned to 144 bytes for the largest variant — while A and AAAA account for more than 80% of traffic and take only a few bytes.
The change that yielded the most gains
Fifth edit — storing records in wire format. Instead of structured fields, the cache now holds a single byte buffer where records are laid out sequentially, each with a two-byte length prefix. That’s exactly how they look in the DNS packet itself.
From here two effects occur immediately. First, eliminates overhead for listing variants. Second, most records are now copied into the response to the client as-is, without re-serialization: you only need to parse the types where there are domain names — CNAME, NS, MX, SOA. And a bonus: locality — consecutive bytes are cheaper for the processor cache than jumping via pointers.
The test data Cloudflare used for measurements was collected according to the distribution of types in production: 56% A, 25% AAAA, 19% TXT. The methodology detail is important — on another traffic profile with many SVCB or DNSSEC signatures, the gains from the scheme of “small types inside, large ones in a heap” will be different.
Side effect: not only smaller, but faster
The cache insertion speed increased by 43%, lookup by 19%. Rollout proceeded gradually: started May 18, 2026, completion across all services by July 6.
The tempting but wrong takeaway here is that “replacing Vec with Box<[T]> speeds up the code.” It wasn’t the type replacement that did the job; it was that data became compact and contiguous under a particular workload pattern. With your code and a different access pattern, the same technique may yield nothing. Cloudflare’s figures are about Cloudflare’s cache.
Why you don’t have 953 bytes — and where to look for yours
Your resolver is not written in Rust and is structured differently, so looking for the same 953 bytes in your setup is pointless. But the memory budget for the cache on your machine is clearly set — almost certainly to a default value.
Unbound. By default msg-cache-size, rrset-cache-size and key-cache-size are 4 MB each. Check actual usage:
unbound-control stats_noreset | grep '^mem\.'
In the output, pay attention to mem.cache.message (memory of the message cache in bytes) and mem.cache.rrset (memory of the RRset cache). If a value approaches the limit — the cache evicts entries before their TTL expires, and some queries go out to the network in vain. The stats_noreset command is specifically for “peek”: unlike stats, it does not reset counters.
dnsmasq. Here cache-size is not in bytes but in names, and by default this is 150 entries — tight for a home router with a dozen devices. Zero disables caching entirely. In debug mode (-d) dnsmasq dumps the full cache on a SIGUSR1 — a handy way to see what it’s actually filled with.
AdGuard Home. The cache_size parameter in AdGuardHome.yaml is in bytes, the same idea — a budget, not a number of names. Nearby live cache_ttl_min and cache_ttl_max, which override upstream TTLs.
What to do in five minutes:
- Capture current usage:
unbound-control stats_noreset | grep '^mem\.'. - Compare
mem.cache.messagewithmsg-cache-sizeandmem.cache.rrsetwithrrset-cache-size. - If you hit the ceiling and memory on the machine is available — raise both limits in multiples (for example to 32m/64m) and restart.
- For dnsmasq — check that the default 150 names isn’t left in place.
What not to do — “fix” a tight cache by extending TTL. The dnsmasq documentation on min-cache-ttl states plainly: artificially extending TTL is generally a bad idea, and the parameter itself is limited to an hour. You are not increasing the cache; you are starting to serve stale answers. On domains with rapidly changing addresses this breaks connectivity exactly when it’s hardest to diagnose.
On the forum thread: Your subdomains are already in a public log — about how DNS leaks even before any cache, and DWG UI: WireGuard + AdGuard Home, if your resolver runs in a Docker stack.
Sources
- How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache — Cloudflare engineering blog, August 27, 2026
- unbound.conf(5) and unbound-control(8) — NLnet Labs
- dnsmasq man page — Simon Kelley
- AdGuard Home: configuration
Has anyone actually tinkered with cache sizes of their resolver after installation — or is everyone running the default 4 MB and 150 names? And how far is your mem.cache.message from the limit?

