How DNS actually works (the practical version)
DNS is one of those things you can ignore until you start self-hosting or configuring domains. Then it becomes essential to understand. The theory is simple but the practical details trip people up, and the "48 hour propagation" myth continues to waste everyone's time.
What DNS does
DNS translates human-readable domain names into IP addresses. When you type example.com in your browser, DNS resolves it to something like 93.184.216.34. Your browser then connects to that IP address. Every single HTTP request starts with a DNS lookup.
The lookup chain
When you request blog.example.com, here is what actually happens:
1. Your browser checks its cache. Chrome, Firefox, and Safari all maintain their own DNS caches. Chrome caches entries for up to 60 seconds. If you visited this domain recently, the answer is already here and no network request is needed.
2. Your OS checks its cache. If the browser cache misses, the request goes to the operating system's stub resolver. This is a minimal DNS client built into the OS. On macOS it is mDNSResponder, on Linux with systemd it is systemd-resolved, on Windows it is the DNS Client service. The stub resolver checks the OS cache and the /etc/hosts file (or C:\Windows\System32\drivers\etc\hosts on Windows).
3. The stub resolver asks a recursive resolver. This is usually your ISP's DNS server, or a public resolver you configured like Cloudflare (1.1.1.1), Google (8.8.8.8), or Quad9 (9.9.9.9). If you run AdGuard Home or Pi-hole on your network, requests go there first, which then forwards to an upstream resolver. The recursive resolver does the heavy lifting. If it has the answer cached, it returns it immediately.
4. The recursive resolver asks the root servers. "Where do I find .com domains?" There are 13 root server identities (a.root-servers.net through m.root-servers.net), but over 1,700 physical instances worldwide, distributed using anycast routing. The root server does not know the IP for blog.example.com. It responds with a referral to the .com TLD nameservers.
5. The recursive resolver asks the .com TLD servers. "Where do I find example.com?" The TLD server does not know the final IP either. It returns a referral pointing to example.com's authoritative nameservers.
6. The recursive resolver asks the authoritative nameserver. "What is the IP for blog.example.com?" This server has the actual DNS records. It returns the A record with the IP address.
7. The answer flows back through the chain. The recursive resolver caches it, the OS caches it, the browser caches it. Each cache respects the TTL (Time to Live) set on the record. The browser then initiates the TCP connection to that IP.
sequenceDiagram
participant Browser
participant Stub as Stub Resolver
participant Recursive as Recursive Resolver
participant Root as Root Server
participant TLD as .com TLD Server
participant Auth as Authoritative NS
Browser->>Stub: blog.example.com?
Stub->>Recursive: blog.example.com?
Recursive->>Root: Where is .com?
Root-->>Recursive: Try .com TLD servers
Recursive->>TLD: Where is example.com?
TLD-->>Recursive: Try ns1.example.com
Recursive->>Auth: IP for blog.example.com?
Auth-->>Recursive: 93.184.216.34
Recursive-->>Stub: 93.184.216.34
Stub-->>Browser: 93.184.216.34
This entire process takes milliseconds and happens before your browser sends its first HTTP request.
A detail that matters: iterative vs recursive queries
The stub resolver makes a recursive query to the recursive resolver, meaning "give me the final answer, I do not care how you get it." The recursive resolver then makes iterative queries to the root, TLD, and authoritative servers, meaning "give me what you know, even if it is just a referral to someone else." This distinction is why the recursive resolver is called recursive: it does the recursion so your computer does not have to.
Record types that matter
A and AAAA records
The fundamentals. A records map a domain to an IPv4 address, AAAA records map to IPv6. The name "AAAA" is because IPv6 addresses are four times the size of IPv4.
example.com. 300 IN A 93.184.216.34
example.com. 300 IN AAAA 2606:2800:220:1:248:1893:25c8:1946
A domain can have multiple A records for round-robin load balancing. The resolver returns all of them and the client picks one (usually the first, but some clients randomize).
If your server supports IPv6, add AAAA records. IPv6-only networks are increasingly common on mobile. But if your server's IPv6 connectivity is broken, having AAAA records is worse than having none. Clients try IPv6 first, time out, then fall back to IPv4, adding noticeable latency.
CNAME record
Creates an alias from one domain to another. The resolver follows the chain and resolves the target.
www.example.com. 300 IN CNAME example.com.
blog.example.com. 300 IN CNAME mysite.netlify.app.
The apex restriction. A CNAME cannot coexist with any other record at the same name. Since your zone apex (example.com itself) always has SOA and NS records, you cannot put a CNAME there. This trips people up constantly because hosting platforms like Heroku, Netlify, and Vercel give you a CNAME target, but you cannot point your bare domain at it.
Solutions: use an A record pointing to the provider's static IP (if they offer one), use your DNS provider's ALIAS/ANAME feature (Cloudflare calls this "CNAME flattening"), or redirect the bare domain to www.
MX record
Specifies which mail servers accept email for your domain. Includes a priority value where lower numbers mean higher priority.
example.com. 300 IN MX 10 mail1.example.com.
example.com. 300 IN MX 20 mail2.example.com.
Mail delivery is attempted at priority 10 first. If that server is down, priority 20 is tried.
TXT record
Holds arbitrary text data. Originally intended for human-readable notes, now heavily used for machine-readable verification and email authentication.
example.com. 300 IN TXT "v=spf1 include:_spf.google.com ~all"
example.com. 300 IN TXT "google-site-verification=abc123..."
TXT records are the workhorse of domain verification. Google Search Console, Stripe, Let's Encrypt, and dozens of other services use them to prove you control a domain.
NS record
Delegates a zone to specific authoritative nameservers. These are usually set at your registrar and rarely change.
example.com. 86400 IN NS ns1.cloudflare.com.
example.com. 86400 IN NS ns2.cloudflare.com.
SOA record
Every DNS zone has exactly one SOA (Start of Authority) record containing administrative information. The most practically relevant field is the minimum TTL, which determines how long negative responses ("this domain does not exist") are cached. If someone tries to visit a subdomain before you create it, that NXDOMAIN response gets cached for this duration.
SRV record
Specifies the host and port for specific services. Used by protocols like XMPP, SIP, and Minecraft servers.
_minecraft._tcp.example.com. 300 IN SRV 0 5 25565 mc.example.com.
CAA record
Specifies which Certificate Authorities are allowed to issue TLS certificates for your domain. CAs are required to check CAA records before issuing.
example.com. 300 IN CAA 0 issue "letsencrypt.org"
This prevents a CA you did not authorize from issuing a certificate for your domain. Worth setting up, especially if you use Let's Encrypt exclusively.
PTR record
Used for reverse DNS lookups: mapping an IP address back to a domain name. The octets are reversed and placed in the in-addr.arpa zone.
34.216.184.93.in-addr.arpa. 300 IN PTR example.com.
PTR records matter for email. Receiving mail servers often check that the sending IP has a valid PTR record. Missing PTR records are a common reason emails get flagged as spam.
TTL: how caching actually works
Every DNS record has a TTL (Time To Live) in seconds. A TTL of 3600 means resolvers cache the answer for one hour. After that, they query again.
Common TTL values
- 60-300 seconds (1-5 minutes): Good during active changes, migrations, or failover scenarios. High query volume but fast updates.
- 3600 seconds (1 hour): A reasonable default for most records. Changes take effect within an hour.
- 86400 seconds (1 day): For records that rarely change, like NS records. Reduces query load but makes emergency changes slow.
The caching layers
Caching happens at every level, and each layer can behave slightly differently:
Browser cache respects TTL but may impose minimums. Chrome caches for at least 60 seconds regardless of TTL.
OS cache varies by platform. macOS's mDNSResponder, Windows DNS Client, and systemd-resolved all maintain their own caches. You can flush them:
# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Windows
ipconfig /flushdns
# Linux (systemd)
sudo systemd-resolve --flush-cachesRecursive resolver cache is the biggest layer. ISP resolvers and public resolvers like 1.1.1.1 cache per the TTL. Some ISP resolvers clamp minimum TTLs (caching for at least 30 minutes even if your TTL says 60 seconds), though this is increasingly rare with major providers.
Application caches can be sneaky. The JVM historically cached DNS indefinitely when a SecurityManager was installed. Modern JVMs default to 30 seconds, but this has bitten a lot of Java developers during migrations.
"Propagation" is not real
This is the biggest DNS misconception. When people say "DNS propagation takes 48 hours," they are describing something that does not exist. DNS is a pull-based system, not push-based. Nothing propagates anywhere.
When you change a DNS record, the change is instant on your authoritative nameserver. But recursive resolvers around the world have cached the old record. Each resolver's cache expires independently based on when it last queried and the TTL of the record. As each cache expires, the next query fetches the new value.
If your TTL was 3600 (one hour), every resolver will have the new record within one hour. If your TTL was 86400 (one day), it takes up to one day. The "48 hours" figure comes from an era when default TTLs were commonly 86400 and some ISP resolvers ignored TTLs entirely. With modern resolvers, this is no longer the case.
The migration playbook
- Check your current TTL with
dig example.comand note the TTL value in the answer - Lower the TTL to 300 (5 minutes) or even 60 seconds
- Wait at least the duration of the OLD TTL (so all caches of the high-TTL record expire)
- Make your DNS change
- The change will be visible to essentially all resolvers within 5 minutes
- Once everything is confirmed working, raise the TTL back up
This is the step most people skip. They change the record without lowering the TTL first, then wonder why some users still see the old IP for hours.
Email authentication: SPF, DKIM, and DMARC
If you send email from your domain (even just transactional emails from your app), you need these three TXT records configured correctly. Without them, your emails land in spam.
SPF (Sender Policy Framework)
Specifies which mail servers are authorized to send email for your domain.
example.com. TXT "v=spf1 include:_spf.google.com include:sendgrid.net -all"
The -all at the end means "reject everything not listed." Use ~all (soft fail) during setup, switch to -all once confirmed.
The 10 lookup limit. SPF has a hard limit of 10 DNS lookups. Each include: directive counts as a lookup, and nested includes count too. Exceeding this causes SPF to return permerror and your emails may be rejected. Use a tool like MXToolbox SPF checker to verify your record.
DKIM (DomainKeys Identified Mail)
A public key published in DNS that receiving servers use to verify email signatures.
selector1._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCS..."
Your email provider generates the key pair and gives you the TXT record to add. The sending server signs outgoing emails with the private key, and receiving servers verify with the public key from DNS.
DMARC (Domain-based Message Authentication)
Tells receiving servers what to do when SPF and DKIM checks fail.
_dmarc.example.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
The p=reject policy means "reject emails that fail authentication." The rua field specifies where to send aggregate reports so you can monitor who is sending email as your domain. Start with p=none to monitor without blocking, then move to p=reject once you are confident everything is configured correctly.
DNS security
DNSSEC
DNSSEC adds cryptographic signatures to DNS responses. Each zone signs its records with a private key, and a hash of the public key is published in the parent zone (as a DS record), creating a chain of trust from the root. When a DNSSEC-validating resolver receives a response, it can verify the signature and reject tampered responses.
Cloudflare makes this a one-click setup. If you are using Cloudflare for DNS, just enable it.
DNS over HTTPS (DoH) and DNS over TLS (DoT)
Standard DNS queries are unencrypted, meaning your ISP (and anyone on the network path) can see every domain you look up. DoH wraps DNS inside HTTPS on port 443, making it indistinguishable from regular web traffic. DoT uses TLS on a dedicated port (853).
Firefox enables DoH by default in the US. Android 9+ supports DoT natively under the "Private DNS" setting. If privacy matters to you, configure your devices or network to use encrypted DNS.
DNS rebinding
An attack that exploits DNS to bypass the browser's same-origin policy. The attacker serves a page from their domain, then changes the DNS to point to an internal IP (like 192.168.1.1). The browser thinks it is still talking to the attacker's domain and allows the JavaScript to read the response from your internal network.
Mitigations include browser minimum TTL enforcement and DNS resolvers that filter out private IP addresses in responses for public domains. Most modern routers and resolvers have rebinding protection built in.
Debugging with dig
dig is the most useful DNS tool. Here are the commands I use regularly:
# Basic lookup
dig example.com
# Query a specific record type
dig example.com MX
dig example.com TXT
dig example.com AAAA
# Short output (just the answer)
dig +short example.com
# Query a specific resolver (useful for comparing)
dig @1.1.1.1 example.com
dig @8.8.8.8 example.com
# Trace the full resolution path (root -> TLD -> authoritative)
dig +trace example.com
# Reverse lookup (IP to domain)
dig -x 93.184.216.34
# Check DNSSEC
dig +dnssec example.comdig +trace is the one to remember. If a domain is not resolving, this shows you exactly where in the chain the lookup breaks. You can see whether the problem is at the root, the TLD, or the authoritative nameserver.
Reading dig output
; <<>> DiG 9.18.18 <<>> example.com
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
example.com. 3600 IN A 93.184.216.34
;; Query time: 23 msec
The key fields: status tells you the result (NOERROR means found, NXDOMAIN means the domain does not exist, SERVFAIL means the server had an error). The flags include aa for authoritative answer and ad for DNSSEC-authenticated data. The Query time tells you if the result was cached (0 msec usually means cached).
Other useful tools: nslookup is available everywhere including Windows. host gives simpler output. doggo is a modern alternative written in Go with color-coded output and native DoH/DoT support. Online tools like dnschecker.org show resolution results from servers worldwide.
Managing DNS for your domain
Your domain registrar usually provides a DNS management interface. For more control and better performance, I use Cloudflare as my DNS provider (free tier). It gives you a fast anycast network, automatic DNSSEC, DDoS protection, and a clean API.
A typical setup for a web application:
# Point your domain to your server
A @ -> your-server-ip
AAAA @ -> your-server-ipv6 (if applicable)
# WWW subdomain
CNAME www -> example.com
# Wildcard for subdomains (multi-tenant apps)
A * -> your-server-ip
# Email (Google Workspace example)
MX @ -> aspmx.l.google.com (priority 1)
MX @ -> alt1.aspmx.l.google.com (priority 5)
TXT @ -> "v=spf1 include:_spf.google.com -all"
Wildcard DNS
A wildcard record (*.example.com) matches any subdomain that does not have an explicit record. This is useful for multi-tenant SaaS apps where each customer gets a subdomain (customer1.app.com, customer2.app.com), or for feature branch deployments in CI.
One gotcha: wildcards only match one level. *.example.com matches foo.example.com but not bar.foo.example.com.
Local DNS
For your home network, a local DNS resolver like AdGuard Home lets you create custom records for internal services:
nextcloud.home -> 192.168.1.50
proxmox.home -> 192.168.1.10
grafana.home -> 192.168.1.30
Devices on your network can access services by name instead of IP address. Much easier to remember and share with other people on your network.
Common mistakes
Not lowering TTL before a migration. The single most common DNS mistake. Change the record with a 24-hour TTL and some users keep going to the old server all day.
CNAME at the apex. You cannot put a CNAME on example.com itself. Use ALIAS/ANAME if your DNS provider supports it, or use an A record.
Multiple SPF records. Having two TXT records that both start with v=spf1 is invalid. You need exactly one SPF record. Combine them using include: directives.
Exceeding the SPF lookup limit. More than 10 DNS lookups in your SPF record causes a permanent error. Audit your includes.
Forgetting PTR records for mail servers. Missing reverse DNS is one of the top reasons emails go to spam.
Ignoring negative caching. If someone visits a subdomain before you create the DNS record, the "does not exist" response gets cached (per the SOA minimum TTL, often 1 hour). They will not see the new record until that negative cache expires.
Sources
Related posts
Tailscale: the easiest way to connect your devices
How Tailscale creates a mesh VPN between your devices without port forwarding, firewall rules, or a VPN server.
Caddy as a reverse proxy for self-hosted services
How Caddy simplifies reverse proxying for self-hosted services with automatic HTTPS and minimal configuration.
Setting up WireGuard for secure remote access
How to set up WireGuard VPN to securely access your home lab and self-hosted services from anywhere.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS