Cloud / SRE
When dig Works but the Browser Does Not: Debugging macOS Split DNS
A practical way to diagnose stale per-domain DNS rules on macOS when one device reaches a site, dig succeeds, but browsers and curl time out.
On this page
A website can work normally from a phone while every browser on one Mac reports that it cannot connect. The first instinct is often to blame the website, CDN, TLS certificate, or browser cache. But there is a more specific failure pattern worth recognizing:
- A phone resolves and opens the site.
digon the Mac returns valid public IP addresses.curl https://example.comstalls during name resolution.- Connecting to a known edge IP with
curl --resolvereaches the site.
When these signals appear together, the website is probably healthy. The Mac is likely sending application DNS queries through a stale split-DNS rule left by a VPN or an earlier network workaround.
Terms used here
| Term | Meaning |
|---|---|
| System resolver | The macOS DNS routing layer used by normal applications, including browsers and curl. |
| Split DNS | A configuration that sends queries for selected domains to a specific DNS server, often through a VPN. |
/etc/resolver | A macOS directory that can define per-domain DNS servers. A file named example.com applies to that domain and its subdomains. |
dig | A DNS diagnostic client. Its result does not necessarily prove that the macOS application resolver is using the same DNS path. |
utun | A macOS tunnel interface commonly created by VPN software. |
curl --resolve | A way to provide a temporary hostname-to-IP mapping for one request while preserving the original hostname for TLS and HTTP. |
The misleading symptom
Start with a public lookup:
dig +short example.com A
dig +short example.com AAAA
If this returns public addresses, it proves that the DNS server queried by dig can resolve the name. It does not prove that Safari, Chrome, or other macOS applications use that answer.
Test the application path separately:
curl -sS -I \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
A result such as this narrows the problem:
curl: (28) Resolving timed out after 8000 milliseconds
The timeout happened before TCP, TLS, or HTTP. Changing Cloudflare rules, origin security groups, or certificates will not fix this stage of the request.
Compare public DNS with the macOS resolver
Use dscacheutil to exercise the system lookup path:
dscacheutil -q host -a name example.com
If dig returns addresses but dscacheutil returns nothing or waits until timeout, inspect the macOS resolver graph:
scutil --dns
Look for a domain-specific resolver:
resolver #8
domain : example.com
nameserver[0] : 192.0.2.53
This means macOS sends example.com and its subdomains to 192.0.2.53, regardless of the default DNS servers learned from Wi-Fi. The address is from a documentation-only range and does not identify a real resolver.
Now inspect persistent resolver files:
if [ -d /etc/resolver ]; then
find /etc/resolver -maxdepth 1 -type f -print \
-exec sed -n '1,20p' {} \;
fi
A file like this creates the rule:
/etc/resolver/example.com
with contents:
nameserver 192.0.2.53
Test that DNS server directly:
dig @192.0.2.53 example.com A \
+time=2 \
+tries=1 \
+noall \
+answer \
+comments
If it times out, inspect the route:
route -n get 192.0.2.53
A route through an inactive or unhealthy utun interface confirms the failure chain:
Browser
-> macOS system resolver
-> /etc/resolver/example.com
-> private DNS server
-> stale or broken VPN path
-> timeout
The phone works because it does not have the Mac’s local resolver file or VPN route.
Prove that DNS is the only broken layer
Take one public IP returned by dig and bypass DNS for a single HTTPS request:
curl -sS -I \
--resolve example.com:443:203.0.113.10 \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
Replace 203.0.113.10 with an address actually returned for the domain.
This keeps example.com as the TLS SNI and HTTP Host value but skips the failing resolver. If the request reaches the CDN or origin, the network and TLS path are working.
A CDN may return a challenge or 403 to command-line curl. That is still useful evidence: DNS, TCP, and TLS succeeded, and the response came from the application-security layer. Treat that challenge as a separate issue from the original resolver timeout.
Recover safely
First record the current state:
networksetup -getdnsservers "Wi-Fi"
networksetup -getsearchdomains "Wi-Fi"
scutil --dns
Create a recoverable backup:
network_backup_dir="$(mktemp -d /private/tmp/macos-dns-backup.XXXXXX)"
echo "Backup directory: ${network_backup_dir}"
For one stale domain, move only that rule:
sudo mkdir -p "${network_backup_dir}/resolver"
sudo mv \
/etc/resolver/example.com \
"${network_backup_dir}/resolver/example.com"
If the machine should have no custom per-domain resolvers at all, back up the entire directory:
if [ -d /etc/resolver ]; then
sudo mv /etc/resolver "${network_backup_dir}/resolver"
fi
Moving the entire directory also removes legitimate internal DNS rules. Use it only when restoring the machine to a fully default resolver configuration is intentional.
If Wi-Fi has manually configured DNS servers and should return to DHCP-provided DNS:
sudo networksetup -setdnsservers "Wi-Fi" empty
Reload the resolver state:
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
If a VPN is still connected but its tunnel is unhealthy, disconnect and reconnect it through the VPN client. Avoid deleting tunnel routes by hand unless the VPN client’s normal cleanup has failed and you fully understand the route ownership.
Validate the recovery
Confirm that Wi-Fi no longer has manual DNS servers:
networksetup -getdnsservers "Wi-Fi"
The expected output is:
There aren't any DNS Servers set on Wi-Fi.
Inspect the active resolver graph again:
scutil --dns
The stale per-domain resolver should be gone. The default resolver will normally show DNS servers supplied by the local network, such as the router address.
Finally, test both the system resolver and HTTPS:
dscacheutil -q host -a name example.com
curl -sS -I \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
At this point, an HTTP response—even a deliberate CDN challenge—shows that the original DNS timeout is fixed.
Roll back if the private resolver was required
Restore the backed-up directory:
sudo mv "${network_backup_dir}/resolver" /etc/resolver
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
scutil --dns
For a single-domain backup, recreate /etc/resolver first if needed and restore only that file.
Design split DNS around the VPN lifecycle
The durable fix is not a recurring cache flush. It is to make resolver ownership match tunnel ownership:
- Install private resolver rules when the VPN becomes usable.
- Remove those rules when the VPN disconnects.
- Scope rules to the narrowest internal domain instead of an entire public parent domain.
- Check private DNS reachability after the tunnel comes up.
- Keep a rollback path for manually installed resolver files.
Adding a public DNS server as a fallback inside a private-domain resolver file is tempting, but it can leak internal names and create inconsistent negative responses. Lifecycle-aware installation is safer than mixing private and public resolvers for the same namespace.
The diagnostic pattern to remember
When one Mac fails while another device works, compare layers instead of clearing every cache:
dig works
+ system lookup fails
+ curl reports "Resolving timed out"
+ curl --resolve reaches the edge
= stale macOS split DNS
That combination turns a vague browser problem into a small, testable configuration issue—and keeps the investigation away from unrelated CDN, TLS, and origin settings.