DNS Cheatsheet
TTL and Caching
Use this DNS reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
What a TTL actually controls
The TTL on a record is how many seconds a resolver may keep serving that answer from cache without re-asking. It is a maximum, not a guarantee.
example.com. 300 IN A 93.184.215.14 ^^^ this record may be cached for up to 300 seconds
The counter starts when the resolver receives the answer, not when you publish it. So the real worst case for a change is the old TTL, from the moment the last resolver cached the old value.
# watch the TTL count down against a caching resolver dig @8.8.8.8 example.com A +noall +answer dig @8.8.8.8 example.com A +noall +answer # same value, lower TTL # a query straight to the authoritative server always shows the full TTL dig @ns1.example.com example.com A +noall +answer +norecurse
A decreasing TTL on repeat queries proves you are reading a cache. A constant TTL means you reached the authority.
Picking a TTL
| TTL | Value | Fits |
|---|---|---|
| 60 | 1 min | Failover targets, records mid-migration |
| 300 | 5 min | A sane default for anything that might change |
| 3600 | 1 hour | Normal web and app records |
| 14400 | 4 hours | Stable records, less resolver traffic |
| 86400 | 1 day | MX, NS, TXT policy records, apex of a static site |
| 172800 | 2 days | Delegation NS at the TLD, set by the registry |
Low TTLs cost query volume and add a lookup to every cold request. High TTLs cost you flexibility. 300 to 3600 covers almost everything.
The migration playbook
This is the whole reason TTLs get attention.
1. At least 48 hours before the change, lower the TTL on the records you will
move. Publish api IN 300 A <old-ip>.
2. Wait out the OLD TTL. If it was 86400, every resolver that cached the old
record before your edit keeps the 86400 value until it expires. Lowering the
TTL does not shorten the copy already in a cache.
3. Verify the low TTL is live everywhere you care about, not just your own
resolver.
4. Make the change. Point the record at the new address.
5. Keep the old endpoint serving for at least the low TTL, doubled. Stragglers
exist: resolvers that ignore TTLs, clients that cached at the application
layer, and JVMs holding a name forever.
6. Raise the TTL back once traffic to the old endpoint is zero.
# step 3, check the low TTL from several resolvers for r in 8.8.8.8 1.1.1.1 9.9.9.9 208.67.222.222; do printf '%-16s ' "$r" dig @$r api.example.com A +noall +answer | head -1 done # step 5, confirm the new value is what resolvers now hand out dig @1.1.1.1 api.example.com +short
Lowering the TTL after you need it is useless. The step people skip is step 1, and then they spend a day wondering why traffic still lands on the old box.
Negative caching
A "does not exist" answer is cached too, and its lifetime comes from the zone's SOA MINIMUM field, capped by the SOA record's own TTL.
$ dig doesnotexist.example.com A ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 5512 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1 ;; AUTHORITY SECTION: example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 2026091501 7200 3600 1209600 300
The 300 at the end of the SOA RDATA is the negative TTL. This NXDOMAIN is
cached for 300 seconds.
| Answer | Cached for |
|---|---|
| Positive (records returned) | The record's own TTL |
NXDOMAIN (name absent) | SOA MINIMUM, capped by the SOA TTL |
NODATA (NOERROR, no answers) | SOA MINIMUM, same rule |
SERVFAIL | Implementation defined, usually 1 to 5 minutes |
Set SOA MINIMUM to 300 or lower. A common failure is publishing a new record right after someone queried the name and got NXDOMAIN, then waiting out a one-day negative TTL. Nothing in the output tells you that is what happened, beyond the SOA in the authority section.
Where else things cache
DNS TTLs only bind resolvers. Four other layers cache independently.
| Layer | Honors TTL | How to clear |
|---|---|---|
| Recursive resolver | Yes | Wait, or ask the operator. Some expose a purge form |
| OS stub cache | Mostly | sudo resolvectl flush-caches (systemd), sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder (macOS), ipconfig /flushdns (Windows) |
| Browser | Partly | chrome://net-internals/#dns then Clear host cache. Chrome caps at about 60s |
| Java / JVM | No, forever by default | networkaddress.cache.ttl=30 in java.security, or -Dsun.net.inetaddr.ttl=30 |
| Connection pools, app-level | No | Restart, or use a client that re-resolves |
# Google and Cloudflare purge forms, useful mid-incident # https://dns.google/cache # https://one.one.one.one/purge-cache/ # check what a specific resolver holds without recursing dig @8.8.8.8 example.com A +norecurse +noall +answer
The JVM one bites hard. A default JVM with a security manager caches a resolved address for the life of the process, so a failover that DNS handled perfectly still sends every Java client to the dead box until it restarts.
Gotchas
- A CNAME chain caches per hop. The final address lives for the target's TTL,
not the alias's, so a 60-second CNAME to a 3600-second A record still pins the
address for an hour.
- dig +short hides the TTL. Use +noall +answer when the TTL is the thing
you are checking.
- Resolvers may clamp your TTL. Many enforce a floor (often 30 to 60 seconds)
and some enforce a ceiling, so a TTL of 1 does not mean per-request lookups.
- dig never reads the OS cache, so flushing the OS cache changes nothing
about dig output and dig proves nothing about what your app sees.
- Registry-level NS TTLs are not yours. The TLD sets the delegation TTL,
commonly 172800, so changing nameservers is a two-day operation regardless of
what your zone says.
- Anycast resolvers have independent caches per site. Repeating a query against
8.8.8.8 from two locations can show two different TTLs and two different
answers.