Skip to content
hollow

Core mechanics

Caching

Iterative resolution from the root costs several round trips and a few hundred milliseconds. Without a cache every query pays it — including the queries the resolver makes of itself while chasing nameserver addresses. Zone operators already published how long each answer may be reused; this is the part that takes them up on it.

TTLs that count down

A general cache is a map with expiry. A DNS cache is not: entries store absolute expiry and every record's TTL is rewritten to the remaining seconds on the way out. A cache that replays the TTL it stored tells every client the answer is as fresh as the moment it was fetched, which defeats their caches as well as being a visible lie — two lookups a minute apart showing the same countdown is the first thing anyone notices.

example.com · 268 ms cold · 0 ms warm

A hit allocates three times and a miss allocates nothing. The three are the TTL rewrite, which is the feature rather than overhead to remove.

Two depths, not one

Final answers are the obvious half. Delegations are the half that makes the difference between names: caching only answers still walks root, com and example.com for every distinct host under example.com, while a remembered zone cut lets the walk start at the deepest zone already known.

A cached delegation redirects every future query for a whole subtree, so only a referral that has already passed its bailiwick check is ever stored. And because zones move, a walk that fails from a cached cut is retried from the root — a single stale cut would otherwise turn into a hard failure for an entire subtree, which is a cache making the resolver worse than no cache at all.

Negative answers

NXDOMAIN and NODATA are cached on the authority of the SOA record that proves them, per RFC 2308: the lifetime is the smaller of the SOA's MINIMUM field and the TTL of the SOA record itself, capped at three hours. No SOA, no entry. A name that does not exist today is exactly the kind of thing that changes, so a zone claiming a week of non-existence is not believed for a week.

Serve-stale

With --serve-stale, an answer that has expired can still be served when resolution fails outright — RFC 8767. It carries a 30-second TTL rather than zero, because a zero TTL tells the client never to reuse the answer, which turns one upstream failure into a query storm from every client relying on the name.

$hollow serve --serve-stale 1h

There is no background refresh, which RFC 8767 recommends. An unbounded set of detached resolutions is the exact failure the bounded worker pool exists to prevent, and the next query for the name retries anyway.

One walk per herd

The cache answers the second query for a name. It does nothing for the second query that arrives while the first is still in flight — and that is the case a DNS server sees constantly: a page load fires a dozen lookups for the same host at once, and a cold name under a sixty-four-wide worker pool could otherwise start sixty-four identical walks from the root, each ending by storing the answer the others were about to store.

Concurrent identical questions collapse into one resolution, keyed on the folded name so two clients spelling it differently share the walk. Each still gets a reply echoing its own question.

Sizing and shape

Default capacity
100,000 answers, about 30 MB
Shards
256, four per possible concurrent writer
Shard selection
hash/maphash with a per-cache random seed
Eviction
least recently used, per shard
Delegation cache
10,000 zone cuts
TTL ceiling
24 hours
Negative TTL ceiling
3 hours

The random hash seed is a defence rather than a detail. With a fixed hash, a client that can name arbitrary domains could compute a set that all land in one shard and collapse a 256-way structure into a single lock.

The cache does not survive the process. A restart starts cold.