blog.rust.gay

How Cloudflare's DNS planner picks the smallest safe build

On 2 November 2026, Cloudflare published an account of a change to the Zone Builder, the service that turns DNS record edits and zone-setting changes into the key-value data its authoritative servers read. The pipeline now takes roughly 700 record changes per second, up from about 250 when the team last wrote about it in 2022, plus around 100 other events such as zone setting changes. Some zones hold more than 20 million records, and Cloudflare expects them to keep growing toward hundreds of millions.

The reported result: full zone builds down by about 40%, peak working-set memory down 85%, and the median time from a change to its data landing in Quicksilver halved from about 7.7 seconds to about 3.3 seconds. The percentages are the least interesting part. The mechanism is the interesting part. Cloudflare built a planner that only takes a shortcut when it can show the shortcut writes exactly the same bytes as the slow path, and that escalates whenever it cannot show it.

What a build is, and why settings broke the safe default

A DNS zone is a set of records plus the settings that govern how those records are served. Cloudflare's path from a customer change to the edge runs through five stages: the DNS Records API or dashboard writes to a database; the database change or another system emits a Kafka event; the DNS Kafka Aggregator merges several topics into one stream; the Zone Builder reads the zone, converts it into Quicksilver keys, diffs against what Quicksilver already holds, and writes only what changed; Quicksilver distributes those keys across the network.

The Zone Builder is not a row copier. It combines records with zone settings, applies DNS serving rules, and arranges the output so an authoritative server can answer a query without scanning the zone. That intermediate representation is where the whole design lives, because it is what the planner reasons about.

A full build had one property worth admiring: it did not need to know why a zone changed. Read the zone and its settings, read every record, encode every record into keys, compare the whole key set against Quicksilver, delete stale keys, write changed keys, update zone metadata. The write at the end is small. The work before it is proportional to the size of the zone, and the entire zone is held in memory while it happens.

In 2022 that cost was already visible. On a test zone with one million records, a full build took about 34 seconds while per-record builds finished in 6 to 8 milliseconds, a factor of roughly 4,250. Averaged across real zones the gap was about 150x. Per-record builds took over ordinary record traffic, and full builds fell to about 13% of all builds, all of them triggered by zone setting changes that the per-record path could not handle.

That 13% is where the current work starts, because it is a share of the count, not a share of the cost. Setting changes are rare; the zones that receive them are not small. A single Universal SSL toggle on a zone with 20 million records reads, encodes, and compares 20 million records in order to change metadata and possibly CAA records at the apex. As zones grow, the residual path becomes the entire latency profile. Cloudflare's framing is that the number of events was never the real problem. The problem was the kind of work some of those events required, and how that work scales with zone size.

Zone settings are harder to target than records for a structural reason. A record event names the record. A zone event can come from any Cloudflare product, frequently does not say why the zone changed, and can alter data shared by several zones that happen to share a public domain name. Cloudflare states the trade-off plainly: choosing too much work wastes capacity, choosing too little can leave stale or incorrect DNS data at the edge. Only the second failure is an outage.

Five build types and one byte-for-byte rule

Quicksilver output for a zone has two parts that behave differently. Record keys hold the answers for one zone and one DNS name. A metadata key, shared by every Cloudflare zone with the same domain name, tells authoritative servers how to use the zone, where to find special records such as nameservers and wildcards, and which internal zone IDs may be consulted when answering. Multiple zone objects for one name are normal during onboarding, offboarding, and other lifecycle transitions; a priority policy chooses a small, bounded set to publish. In the post's example, four internal zones exist for example.com and the metadata key lists three of them.

That split produces five build types:

Build type What it rebuilds Typical trigger
Resources only Records at specific DNS names A, AAAA, TXT, CNAME edits
Resources and metadata Specific names plus the shared key Nameserver, wildcard changes
Apex and metadata Zone apex plus the shared key Universal SSL and similar settings
Purge Removes a zone that should not be published Zone disabled on this network
Full zone Every record in the zone Anything the planner cannot bound

The safety rule is one sentence and it is the entire design: every key a targeted build writes must be byte-for-byte identical to what a full build would write. If that cannot be demonstrated, the planner does a full build. This is a much stronger statement than "the diff is probably fine". It is an equality claim about output, and equality is something you can test mechanically rather than argue about in review. It is also what makes the mirror comparison described below meaningful: if targeted output must equal full output, then a deployment producing targeted builds can be diffed against one producing full builds.

Targeted builds must also remove state, not just write it. Record events carry deleted records and the previous version of updated ones, and the Zone Builder scans Quicksilver for existing keys under each name it rebuilds, deleting anything the new build did not write. The 2022 experience explains why this is spelled out: per-record updates originally carried only the new record, so renaming a record left the old key behind until updates were split into a create and a delete. Cleanup is the part of a targeted build that fails quietly and poisons answers months later.

Escalation is deliberately blunt. A change that can affect records anywhere in the zone gets a full build. A zone that may need to be published for the first time gets a full build. A missing or unknown event detail gets a full build. A timestamp-only database update that says, in effect, "something about this zone changed" gets a full build until the team can identify the caller and its purpose. Cloudflare's line is that the planner never turns uncertainty into less work, and the code follows the sentence.

There is one deliberate exception, and it is the most interesting decision in the post. Above a fixed record-count threshold, the Zone Builder never escalates to a full build. It rebuilds the apex, the metadata, and every name identified by the queued events instead. The reasoning is liveness, not elegance: a full build of those zones cannot finish, and while it retries it holds back every other change on its Kafka partition. Cloudflare accepts the trade because a build that never terminates is worse than a bounded build that might miss something, and because many of the cases that would otherwise need a full build involve several zones sharing one name, which zone holds make less likely by preventing the same name from being added to another account. For the rare cases that remain, such as enabling a zone on another network, engineers can rebuild individual names by hand.

That exception is worth sitting with. Everywhere else the system prefers a complete but expensive answer; at the top of the size range it prefers an incomplete but terminating one. Both are correct choices for their regime, and the threshold is the admission that no single policy covers 20 million records and 20 records at once.

plan(event, zone):
    if event declares no change                  -> full(zone)
    if change can touch names outside the queue  -> full(zone)
    if zone may need first publication here      -> full(zone)
    if record_count(zone) > threshold            -> apex + metadata + declared names
    else -> smallest of {resources, resources+metadata, apex+metadata, purge}

That sketch paraphrases the rule the post describes; it is not Cloudflare's code.

Planning twice, scheduling once, committing safely

An event describes the past. A build may wait in a queue while more events arrive and the database moves underneath it, so planning happens twice. The first pass turns the event payload into a conservative work list. The payload now includes the changed zone fields and settings, with previous and new values where available, and each setting was traced through the code that generates DNS output and classified by what it can affect.

The second pass happens immediately before execution and reads a fresh publication snapshot: the current zone settings, whether this Zone Builder should publish the zone to its network, and which same-name zone IDs belong in the metadata key. A network here is an isolated copy of Quicksilver for one environment. Each Zone Builder deployment writes to exactly one, and a zone can be enabled on some networks but not others, which is how a domain can be served from both the global network and the China Network.

Fresh state can change the outcome four ways. A targeted build can escalate to a full build if the zone has acquired a feature that requires whole-zone work. A build can become a purge if the zone should no longer be published to this network. A build can be skipped if the triggering zone no longer belongs in the metadata key. And a metadata build can reconstruct the shared key from the current candidate set rather than from the set that existed when the event fired.

Those reads go to database replicas, so builds do not compete with customer writes. Kafka delivery and replication move independently, which means an event can arrive before its database change is visible on a replica. The DNS Kafka Aggregator delays events long enough to cover normal lag, and a final database check rejects the read if the replica is still behind. The ordering inside a build matters too: when a build writes the metadata key it rebuilds the whole key from the fresh view instead of patching an older copy, and it writes the triggering zone's records before the metadata that points at them. Both rules ensure that a partially applied build points at data that already exists rather than data that is about to.

Scheduling was consolidated at the same time. Record work and full-zone work previously ran on separate schedulers, each able to merge its own work but not the other's. A single scheduler now takes the planner's decisions and merges events for the same zone: an A record change at www.example.com plus a wildcard change at *.example.com become one build covering both names and the metadata update. Merging only ever adds work. If any queued event requires the complete zone, the merged build is a full build, and a purge overrides every rebuild because its desired state is that the zone is not published at all.

Kafka correctness is where the scheduler earns its keep. Messages are ordered within a partition, and Kafka stores one committed offset per partition: the point before which every message is known complete. Advancing that offset past unfinished work could lose a customer change on restart. Cloudflare uses soft committing, so a build can finish out of order and be marked complete in memory while the committed position waits for every earlier message. A restart therefore replays work that was already done. That is a deliberate choice of repeated work over lost changes, and it is why targeted plans help twice: less work per build means the oldest unfinished message clears sooner, and the partition offset moves sooner. A per-zone rebuild interval, which differs by plan, stops one busy zone from occupying every worker; changes arriving inside the interval are merged into the next build, and old queued work is allowed to run before the interval blocks a partition for too long.

Proving equivalence before trusting the planner

A smaller build is only useful if it produces the same DNS data, so the team built two kinds of evidence. The first is equivalence testing. For the nameserver, wildcard, and apex paths, a test runs a full build and a targeted build from the same database state, pins time-dependent values, and compares every Quicksilver key owned by the targeted plan byte for byte. Planner and scheduler tests separately cover classification, same-name zones, merging, purges, and escalation. Unit tests cannot enumerate every customer configuration, but they can pin the rule that matters: same input state, same output bytes.

The second is a mirror. Every release runs through a Zone Builder that consumes the production Kafka stream and reads production database state but writes to a separate Quicksilver environment that serves no customer traffic. A comparer samples 20% of zones and checks for missing keys, extra keys, and differing values. The published screenshot shows a 0.31% diff rate across 234,313 matched zones. A mismatch is not automatically a regression, because the two environments do not build a zone at the same instant; the team looks for new kinds of difference that persist after both pipelines catch up. The dashboard that groups those differences runs on Workers with Durable Objects for state and Cloudflare Access in front, which is a neat piece of dogfooding but also the least transferable detail in the post.

During rollout they went further and ran a full build alongside every targeted build, comparing output. That is expensive by design, reading the complete zone every time, and it was retired once the mirror comparison provided the same confidence at lower cost. The rollout itself was keyed on a hash of the zone name rather than the zone ID, so every zone sharing a name landed on the same scheduler and two schedulers could never write the same metadata key. Free zones went first, then paid ones.

The ordering of that evidence is the part worth copying: prove the transformation locally, shadow it in production, sample the differences, classify them, and only then move traffic by a hash that respects shared state.

Reading the numbers honestly

Over a 24-hour window, Cloudflare measured this distribution of build types:

Build type Share of builds
Resources only 86.81%
Full zone 7.04%
Resources and metadata 2.54%
Apex and metadata 2.30%
Purge 1.30%

Targeted work plus purges account for 92.95%, so full builds are the remaining 7.04%. The 40% figure comes from arithmetic on the two metadata build types: they were full builds before this work, they now account for 4.84% of builds, and 4.84% plus 7.04% is 11.88%, against 7.04% today. That is a relative reduction of about 40% in the number of full builds. Because those metadata builds disproportionately hit the largest zones, the count understates how much work was avoided. The full builds that remain average 0.35 seconds, because the events that still trigger them, such as first publication, mostly involve new zones with few records.

The tail is where the change shows up. Cloudflare measured build completion delay, defined as the time from a record or setting change until the Zone Builder finishes writing to Quicksilver, and explicitly excludes Quicksilver's distribution across the network. Production ran the old schedulers while the mirror ran the unified scheduler on the same events over the same 24 hours, reading the same database, writing equivalent data. That is about as controlled as a comparison gets without a synthetic benchmark. The largest p99 spike fell from 14.6 minutes to 1.21 minutes, a twelve-fold reduction, and the Kafka consumption chart stops swinging between roughly 300 and 1,550 messages per second and instead tracks production at 600 to 800.

Memory is the cleanest result. Aggregate working-set memory for the deployment fell from an average of 49 GiB to 7 GiB, an 86% reduction, and peaks fell from 79 GiB to 12 GiB, an 85% reduction. The mechanism is obvious once you see it: a full build holds every record from the database and every existing Quicksilver key for the zone at once, so the peak is a function of the largest zone being rebuilt. Stop rebuilding 20-million-record zones for setting changes and the peak collapses. This is also why the number is not a general claim about Zone Builder efficiency; it is a claim about how often the biggest possible object was being materialized.

The headline "halved build delay" needs a caveat that the post itself supplies. Median completion delay fell from about 7.7 seconds to about 3.3 seconds, but a large share of that time is the per-zone rebuild interval, not build work. The halving came from revisiting two safeguards that the new headroom made affordable: the rebuild interval was shortened on most plans, and the Kafka aggregator's hold time was reduced once the databases began rejecting reads from replicas that had fallen behind. The chart shows the first step down to roughly 6.4 seconds from the aggregator change, then two further steps to about 3.3 seconds as the Zone Builder release rolled out. So a meaningful part of the win is policy relaxation enabled by cheaper builds, not raw build speed. That distinction matters if you try to reproduce the number: shorten your safety margins only after the work behind them gets cheaper.

There is a second caveat, and it is refreshing to see it published. The two-hour chart used histogram buckets at 1, 5, 10, and 30 seconds, and a percentile falling between two edges gets estimated as if delays were spread evenly across the range. Because most delays sit near the bottom of their bucket, the estimates come out high: p99 read close to 30 seconds and p90 above 7 seconds. The exact average, which does not depend on buckets, confirmed the halving. Cloudflare has since moved to native histograms, which put p90 at about 5.5 seconds and p99 at about 12 seconds, with a median around 3.4 seconds. Reporting the bucket artifact rather than quietly swapping charts is the kind of detail that makes the rest of the numbers easier to trust.

What the post does not give: any per-zone latency guarantee, any dollar figure for the capacity saved, and any measurement of Quicksilver distribution, which it says is unaffected. It also does not claim that targeted builds are faster than per-record builds; the comparison here is against full builds, and the 150x and 4,250x figures belong to the 2022 work.

What other teams can copy

Strip away Quicksilver, Kafka, and the DNS specifics and what remains is a reusable pattern for any system that maintains a derived artifact from a database: cache invalidation, search indexes, materialized views, CDN configuration, feature-flag snapshots, routing tables.

Make producers declare intent. The single highest-leverage change was teaching event producers to include which fields changed, with old and new values where possible, and then tracing every setting through the code that generates output to classify its blast radius. That is unglamorous work in someone else's service, and it is what makes every later optimization possible. The corollary is just as important: when the declaration is missing, treat it as "everything changed". A planner that guesses is worse than a full rebuild.

Define correctness as output equality. The byte-for-byte rule converts a distributed-systems argument into a test. Any team building an incremental path can adopt it: run both paths from the same snapshot, pin the clocks and random seeds, and compare the full output rather than the fields you believe are affected. If your incremental path cannot be compared this way, you do not yet know whether it is correct.

Let merges only add work. Combining queued work is an optimization that must never narrow scope. A full build absorbs the merge, a delete overrides a write, and an unknown escalates. This is a lattice, and getting the join operation right is easier than reasoning about interaction cases one at a time.

Plan twice, with a lag guard. Decide conservatively when the event arrives, then re-read the authoritative state immediately before writing, from a replica that is checked for lag. Every system that builds from change streams eventually meets the race between stream delivery and storage replication; the fix is a cheap freshness check at the last moment, not a longer sleep.

Shadow the expensive path and diff a sample. The mirror deployment is the most portable idea in the post. You do not need a second Quicksilver: you need a second writer that consumes the same input, writes somewhere harmless, and a comparer that reports difference classes rather than a raw count. Sampling 20% of zones is enough to catch systematic errors and cheap enough to run continuously.

Shard on the key that owns shared state. Hashing the zone name rather than the zone ID kept every zone sharing a name on one scheduler, which is what made concurrent metadata writes safe. Whatever your shared object is, route by its identity, not by the identity of its tenants.

Prefer replay over loss. Soft committing means work is redone after a restart rather than skipped. For configuration data that is almost always the right bias, and it costs less than it sounds once builds are small.

Finally, accept bounded inefficiency. Cloudflare explicitly abandoned the 2022 goal of eliminating full builds, calling it neither realistic nor useful. Full builds remain for first publication, for changes that can touch anything, and for events nobody has classified yet. Above a record-count threshold the system refuses to escalate at all, because a build that cannot finish is worse than one that is merely incomplete. That is not a failure of the design; it is the design admitting where its proof stops.

The number to watch in a system like this is not the 92.95% that avoided the expensive path. It is the 7.04% that still takes it, and whether each of those cases is understood. A planner that never escalates is not optimized; it is unverified.

Sources


← back to posts