System design interview
Design a caching DNS resolver
The recursive walk, TTLs, negative caching, serve-stale and poisoning defences.
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Cache and queue
- A phone asks for the address of www.example.com.
- The guard checks that the phone is one of ours and under its rate limit, then passes the question on.
- The resolver looks for the answer, and for the closest name servers it knows. The cache holds neither, so it starts at the top.
- It asks a root server, using built-in addresses. The root refers it to the .com servers and names their addresses.
- It asks a .com server, which refers it to the name servers that hold example.com.
- It asks one of those servers, which answers with the address and a TTL of 300 seconds.
- The resolver stores the answer, and the referrals, each with its own expiry time.
- It answers the phone through the guard. The next lookup under .com will skip the root.
- Client
- Query guard: ACL, rate limit
- Resolver: recursive lookup
- Answer cache: TTL per record
- Prefetcher: warms hot names
- Root servers
- TLD servers
- Zone servers: hold the answer
On this page
In 2 minutes
I would build a recursive resolver around one rule: every answer is cached for exactly as long as its owner allows. A query guard accepts only our own address ranges and rate limits each client, which keeps us from being used for amplification. The resolver checks its cache, and on a miss walks from a root server to the top-level domain servers to the zone's own servers, following referrals and caching each reply with its TTL. Because the name servers for popular zones stay cached, a miss usually costs one upstream query.
At 5 million subscribers and 100 queries each, we get about 5,800 queries a second, and with an 85% hit rate only about 870 misses. We pass on the remaining TTL, not the original, and clamp extremes. Names that do not exist are cached too, for the SOA's minimum time. A prefetcher refreshes popular names just before they expire, and if the zone servers are down we serve the expired answer with a 30 second TTL.
Against poisoning we use random query IDs, random source ports, random letter case and strict matching of the question and server, plus optional DNSSEC validation. The trade-off is that serve-stale and clamped TTLs can briefly disagree with the owner's latest data.
Step 1 of 6: Clarify the scope
Interviewer asks
Design a caching DNS resolver, the service that turns a name such as www.example.com into an address for the phones and laptops of a few million subscribers. Answers must come back in milliseconds, stay correct when site owners change their records, and survive attackers who try to feed the cache false answers.
Questions to ask first
Ask these before you draw anything. Under each one is why its answer changes the design.
- Who may ask: our own subscribers only, or anyone on the internet?
A resolver open to everyone is a tool for amplification attacks. Restricting clients is the first defence.
- Do we only resolve names, or also host zones for our customers?
Hosting zones is the job of an authoritative server, a different system. This design is the recursive, caching side only.
- Which record types matter: addresses only, or also aliases, mail and text records?
Aliases (CNAME) mean one question can need several lookups, and every type has its own TTL.
- Is validating answers cryptographically (DNSSEC) required, or optional?
Validation adds signature checks, extra queries and new failure modes when a zone is signed badly.
- What should happen when a site's name servers are unreachable?
Failing the lookup breaks every page. Serving a slightly old answer keeps most of them working.
Requirements
Functional
- Answer a question for a name and a record type, over UDP with a fallback to TCP.
- Find the answer by asking the root, then the top-level domain, then the zone's own servers, and cache every step.
- Follow aliases, and report names that do not exist.
- Answer only our own clients, and rate limit each one.
Non-functional
- A cached answer returns in under 1 ms at p99, and a full lookup in under 300 ms at p95.
- Honour each record's TTL, clamped to between 30 seconds and a day, and never serve past that unless serve-stale is on.
- Keep serving about 85% of queries when every upstream server is unreachable.
- A forged answer is never cached: the ID, port, name and server must all match.
- 99.99% availability, using at least 3 resolver nodes.
Step 2 of 6: Size it
The cache absorbs about 85% of all lookups, and every answer's expiry is set by someone else's TTL. So the work is in respecting TTLs exactly, caching bad news as well as good, and refusing anything that looks forged.
Assumed for the exercise, not facts about a real system:
- 5 million subscribers send 100 queries a day each. Peak traffic is 3 times the daily average.
- 85% of queries are answered from the cache. A miss needs 1.5 upstream queries on average, because popular zones' servers are already cached.
- An upstream round trip is about 40 ms. A cache hit takes about 0.2 ms to serve.
- The cache holds 20 million records at about 250 bytes each, counting the record and its bookkeeping.
- Queries per second
- 5M × 100 = 500M a day; 500M ÷ 86,400 s ≈ 5,790
- ≈ 5,800/s, 17,400/s at the peak
- Cache misses per second
- 5,800 × 15%
- ≈ 870/s, 2,600/s at the peak
- Upstream queries per second
- 870 × 1.5
- ≈ 1,300/s, 3,900/s at the peak
- Average answer time
- 85% × 0.2 ms + 15% × (1.5 × 40 ms = 60 ms)
- ≈ 9 ms; with no cache at all every query would walk root, TLD and zone, about 120 ms
- Cache memory
- 20M × 250 B
- ≈ 5 GB, which fits in the RAM of one node
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
A Client sends its question to the Query guard, which accepts only our own address ranges and applies a per-client rate limit. Permitted queries pass to the Resolver, which does the real work.
The resolver first looks in the Answer cache. It looks for the answer itself, and if that is missing, for the closest name servers it knows for the name. On a miss it walks downward: a Root server refers it to the TLD servers for the top-level domain, and those refer it to the Zone servers that hold the answer. Each reply is stored with its TTL, so later lookups skip steps.
A Prefetcher watches which names are popular and asks the resolver to refresh them just before they expire. If the zone servers stop answering, the resolver falls back to an expired answer for a limited time.
Other paths through the system
Answer from the cache
- A phone asks for a popular name that another subscriber asked for a minute ago.
- The guard lets the question through.
- The resolver looks in the cache and finds the address, which expires in 240 seconds.
- The cache returns it. Nothing was sent upstream.
- The resolver answers with a TTL of 240, not 300, so the phone's own cache never outlives ours.
Refresh early, serve stale
- A name was asked for 5,000 times today, and its entry has 20 seconds left. The prefetcher asks the resolver to refresh it.
- The resolver asks the zone servers directly, because their addresses are cached and no walk from the root is needed.
- The servers do not answer. After a short timeout the resolver tries the next one, then waits and tries again later.
- The old entry has expired, but it is kept for up to a day, marked stale.
- A phone that asks now gets the stale address with a TTL of 30 seconds, so it asks again soon.
Step 4 of 6: Go deep on the hard part
The recursive walk and its caches
Interviewer asks
How does a resolver find an answer it has never seen, and why does it get faster over time?
A phone runs a small stub resolver that sends one question and waits.
Read moreShow less: The recursive walk and its caches
All the work happens in our recursive resolver. A recursive resolver starts from a built-in list of root server addresses and follows referrals.
- The root servers do not know example.com, but they know who runs .com. The reply carries NS records, which name the .com servers, and glue records, which give those servers' addresses.
- A .com server does the same one level down, and names the servers for example.com.
- A zone server finally answers with the record itself, and the TTL the owner chose.
Glue exists to break a loop. If the servers for example.com are called ns1.example.com, nobody could find their address without already knowing the address of example.com. So the parent zone hands out the address along with the referral.
Every reply is cached under its own TTL. The NS records for .com are usually cached for a day or two, so after the first lookup the root is skipped for every .com name. The NS records for a busy zone are cached too, so a miss on a popular site is often a single query to its own server. That is why we assumed 1.5 upstream queries per miss and not 3.
Aliases. A CNAME says that one name is another name. The resolver caches the alias and then resolves the target. Cap the chain at about 8 steps, and the whole query at about 20 upstream queries, so that a loop or a hostile chain of delegations cannot make one question cost a thousand.
Transport. Queries use UDP, with a few tries on different servers, preferring the one with the lowest measured round trip. When an answer is too large, the server sets a truncated flag, and the resolver repeats the question over TCP. Advertising a modest buffer size, around 1,232 bytes, avoids fragmented packets. Query minimisation helps privacy: ask the root only for the top-level label (com), not the whole name, since it only needs to refer us.
TTLs, negative answers, prefetch and serve-stale
Interviewer asks
How do you keep cached answers correct as records change, and what do you do when the source is down?
The TTL is the contract. The owner of a record says how long others may keep it.
Read moreShow less: TTLs, negative answers, prefetch and serve-stale
Store an absolute expiry time, and when answering return the remaining TTL, not the original. A record that has 240 seconds left should be passed on with 240. Otherwise each layer would restart the clock, and a 300 second record could live far longer than its owner intended. Clamp extremes: raise very low TTLs to about 30 seconds, so a zone cannot force a lookup for every query, and cut very high ones to a day, so a mistake cannot be stuck in our cache for a month.
Negative caching. A name that does not exist (NXDOMAIN) or a type that has no data is an answer too. Cache it, for the lower of the TTL on the zone's SOA record and the SOA's minimum field. Without this, a mistyped name or a dead tracking domain is looked up again on every request. The cost is that a name just created stays missing until the negative entry expires, which is why owners add records before they announce them.
Prefetch. A popular name's entry expires every few minutes, and the next user to ask pays a full lookup. Instead, when a name has been hit several times and has under 10% of its life left, refresh it in the background. Do it only for names that have earned it, or you will refresh thousands of names nobody wants. This trades some extra upstream traffic for steady latency on the names that matter most.
Serve-stale. When every attempt to reach a zone's servers fails, an expired answer is usually still correct. Keep expired records for up to a day, and return one with a short TTL of about 30 seconds while retrying in the background. An outage at one site's DNS provider then breaks far fewer pages. The cost is that a record changed during the outage may be wrong for a while.
Changing a record safely. Owners who plan a move lower the TTL a day ahead, since resolvers only see the new TTL when the old entry expires. They change the record, wait, and raise the TTL again.
Poisoning and abuse
Interviewer asks
How could an attacker plant a false answer in the cache, and how do you make that hard?
A resolver sends a question over UDP and accepts the first reply that looks right.
Read moreShow less: Poisoning and abuse
An attacker who does not control the path can race: send many forged replies while the real one is on its way. A false address that lands in the cache is served to every subscriber until it expires. That is cache poisoning.
The forger must guess what the resolver is waiting for. So make it hard to guess:
- A random 16-bit query ID. A guess has a chance of 1 in 65,536.
- A random source port for each query, from about 60,000 ports. Together with the ID that is about 4 billion combinations, not 65,000.
- Random letter case in the question (such as wWw.ExAmPlE.com). A real server copies the case back, and a forger must guess it too.
- Match everything. The reply must come from the server we asked, to the right port, with the same question section. Ignore unsolicited replies.
- Accept only data the server may speak for. A server for example.com may not tell us about the address of another domain. Discard those extra records.
DNSSEC adds proof, not just luck. Zone owners sign their records, and each parent zone vouches for its child's key, forming a chain up to the root key we already hold. The resolver checks the signatures and rejects a failure with a server error, never an unsigned guess. It is optional because it costs extra queries and CPU, and a zone with a broken signature becomes unreachable for validating resolvers. Turn it on, monitor the failure rate, and have a way to relax it for a single broken zone.
Reflection and amplification. A UDP source address can be forged. If we answer anyone, an attacker can send a 60 byte question with a victim's address and make us send a 3,000 byte answer there, 50 times larger. So answer only our own ranges, rate limit each client and each destination, and cap the size of responses to unverified sources. A response rate limit that slows identical answers to one address is the standard guard.
Keep the cache small enough to fill from memory, cap entries per name, and limit work per client, so that one subscriber who asks for a million random names cannot push out everyone's useful records.
Step 5 of 6: Name the trade-offs
TTL handling
Honour TTLs, clamped to between 30 seconds and a day over Use each TTL exactly as sent
A tiny TTL cannot turn every query into an upstream lookup, and a huge one cannot pin a mistake. The cost is that we sometimes ignore what the owner asked for.
When upstream is down
Serve an expired answer for a short TTL over Return a failure
Most pages keep working through someone else's outage. The cost is that a record changed during the outage can be wrong for a while.
Hot names
Prefetch before they expire over Refresh only when someone asks
Popular names never cause a slow lookup. The cost is extra upstream traffic, so only names with repeated hits qualify.
Answer validation
Validate DNSSEC where a zone is signed over Trust the match on ID, port and name alone
A forged answer is rejected even if the forger wins the race. The cost is CPU and extra queries, and zones with broken signatures stop resolving.
Step 6 of 6: Say what breaks next
Questions an interviewer asks once the design is on the board. Answer each one out loud first.
- How would you run many resolver nodes without every node asking upstream for the same names?
Put the nodes behind a load balancer, and add a shared cache tier that front nodes ask before going upstream, with names spread over it by hash. A popular name is then fetched once for the whole fleet. The front nodes keep small local caches for the very hottest names.
- How do you block known malware domains?
Check each name against a blocklist before the cache, and answer with a not-found or with the address of a warning page. Cache the policy result separately from real answers, so removing a name from the list takes effect at once. Log hits, because they point at infected devices.
- What changes if devices want encrypted DNS?
Add listeners for DNS over TLS and DNS over HTTPS in front of the guard. The resolver and its cache do not change. Query logs become more sensitive, so keep them short-lived and anonymised.
- A site moves to a new address but some users still reach the old one. Why?
Caches keep the old record until its TTL runs out, and our resolver, the phone and sometimes the browser each keep their own copy. The owner should lower the TTL a day before the move. Some resolvers also ignore low TTLs, which we cannot control.