DNS Split-Horizon for CDN: Different Answers for Internal and External Queries

I've lost count of how many times I've watched an internal build job fetch a video segment from our own CDN domain and take an 80ms trip to an edge POP two states away, when the origin was sitting in the next rack. That's the classic symptom of a flat DNS setup where your internal DNS and external DNS hand out the exact same answer to everyone. With split-horizon DNS, you stop doing that — you give internal clients a direct, private answer and external clients the optimized CDN answer. It sounds simple on a whiteboard, but getting it right shaves real latency and a surprising amount of egress cost.

How Split-Horizon Actually Answers Different Clients

At its core, split-horizon DNS isn't a separate protocol, it's a way of configuring your authoritative servers to serve two (or more) versions of the same zone based on who's asking. Most implementations use what BIND calls "views" and what other platforms call split-brain or partitioned zones. When a resolver queries for assets.example.com, the server checks the source IP against an ACL before deciding which zone file to use. An external resolver on the internet gets the CDN-hosted set of records — usually a CNAME or a set of anycast A/AAAA records — while an internal resolver on your corporate or VPC network gets RFC1918 addresses or a private origin CNAME that never leaves your network.

I learned how sensitive this is the hard way back in 2018. We’d set up a new staging environment and reused the production zone file without splitting it. For three days, our internal health checks were resolving the public CDN hostname, hitting the external load balancer, and then hair-pinning back inside through the NAT gateway. Our dashboards looked fine, but we were paying for bandwidth twice and our p95 origin fetch time doubled. Splitting the zone wasn’t just a neat trick; it fixed a routing loop we didn’t even realize we’d created. If you’re delegating parts of your architecture to the edge, you should understand how DNS NS Delegation for CDN: Offloading Domain Resolution to Edge Networks interacts with those views, because a delegated subdomain won’t automatically inherit your split logic unless you configure it on the delegated nameservers too.

For a CDN workload, the split is almost always about keeping internal traffic off the CDN entirely. External users should get the benefit of Anycast, GeoDNS, and health-checked edges, but your CMS editors, your transcoders, or your own monitoring agents shouldn’t. They don’t need to be routed to the nearest POP — they need the shortest path to the origin or to a private cache cluster. That’s where split DNS CDN patterns shine. You’re not trying to be clever with routing; you’re preventing internal clients from paying the performance tax that external clients gladly pay to get closer to content.

// BIND 9 split-horizon with two views - simplified
acl "internal-nets" { 10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16; };

view "internal" {
  match-clients { "internal-nets"; };
  recursion yes;
  zone "assets.example.com" {
    type master;
    file "/etc/bind/zones/internal/assets.example.com.db";
  };
};

view "external" {
  match-clients { any; };
  recursion no;
  zone "assets.example.com" {
    type master;
    file "/etc/bind/zones/external/assets.example.com.db";
  };
};

Designing Internal and External Zones for CDN Traffic

The files behind those views look deceptively similar, but they serve completely different purposes. Your external zone is what the world sees, and it should be optimized for CDN delivery. That usually means short TTLs on the apex, a CNAME to the CDN provider, or flattened A records if you’re at the root. Your internal zone, on the other hand, should be boring and direct. I keep internal TTLs much longer — 300 to 900 seconds — because internal clients aren’t moving between POPs, and a longer TTL reduces chatter on internal resolvers without hurting failover. The key is to never mix the two. If an internal client accidentally gets the external CNAME, you’ve just forced it out to the internet for no reason.

This distinction matters even more when you’re working at the zone apex. You can’t put a standard CNAME at example.com without violating RFC 1912, so many teams rely on provider-specific tricks to make it work externally while keeping it clean internally. I’ve had good results keeping the internal apex as a plain A record pointing to the origin load balancer and letting the external apex use the provider’s aliasing. For a deeper look at that exact trade-off, check DNS CNAME Flattening: Pointing Root Domains to CDN Without Breaking DNS Rules. And if you’re tuning how HTTP itself behaves after DNS resolves — like connection reuse, caching headers, and prioritization — the HTTP Working Group Specifications are still the most reliable reference for what’s actually happening after that DNS answer is returned.

Don’t forget that modern CDNs also rely heavily on EDNS Client Subnet (ECS) to make better GeoDNS decisions for external clients. Your internal view should strip or ignore ECS, because your internal resolvers don’t need to tell the CDN where the client is — you already know it’s inside. A common mistake I see is forwarding internal queries to an external resolver that adds ECS, which then pollutes the CDN’s mapping. Keep the paths separate: internal resolvers forward to your internal authoritative servers, external resolvers get the ECS-aware answer from the CDN’s managed DNS.

; --- INTERNAL zone: /etc/bind/zones/internal/assets.example.com.db ---
$TTL 600
@   IN  SOA  ns1.internal.example.com. hostmaster.example.com. ( 2024010101 3600 600 86400 600 )
    IN  NS   ns1.internal.example.com.
    IN  NS   ns2.internal.example.com.
    IN  A    10.20.30.15          ; private origin LB - no CDN hop
cms IN  A    10.20.30.20

; --- EXTERNAL zone: /etc/bind/zones/external/assets.example.com.db ---
$TTL 60
@   IN  SOA  ns1.example.com. hostmaster.example.com. ( 2024010101 3600 600 86400 60 )
    IN  NS   ns1.example.com.
    IN  NS   ns2.example.com.
    IN  CNAME  assets.example.net.cdnprovider.net. ; routed via CDN Anycast/GeoDNS
    IN  TXT   "internal split-horizon active - external view"

The Performance and Security Trade-offs You Can't Ignore

Splitting horizons adds complexity, and complexity has a cost you can measure. On the plus side, internal resolution gets dramatically faster because you’re cutting out the external hop and avoiding the CDN’s full resolution chain. I’ve measured this on three different VPC architectures and the internal path is consistently 15-30ms faster for the DNS lookup alone, before you even count the avoided HTTP round trip. You also get a security benefit: internal hostnames, service discovery records, and origin IPs never leak to the public internet. That’s not just tidy, it’s a real reduction in attack surface. The downside is operational overhead. You now have two zone files to keep in sync for any truly public record, two sets of serial numbers to bump, and two places where a typo can cause an outage that only affects one audience, which makes it harder to catch.

You’ll also want to watch caching behavior closely. External CDN DNS answers are designed to be cached briefly at the edge and by ISP resolvers — a 60-second TTL lets the CDN shift traffic quickly during a POP failure. Internally, you don’t want that churn. For guidance on how DNS TTLs interact with browser and edge caching, the web.dev Articles on caching and performance are more practical than most DNS vendor blogs. And if you *do* want external GeoDNS to be precise, you’ll need to think carefully about how your split interacts with EDNS Client Subnet for CDN Geolocation: Routing Users to the Nearest Edge — ECS gives the CDN a /24 of the client, but your internal view should never expose that subnet externally.

Before you roll this out, instrument both views separately. Don’t look at a blended DNS latency graph; split your metrics by view. That’s how you catch the one misconfigured ACL that sends half your office through the CDN and the other half directly to origin.

Metric (Measured from Internal VPC Client) Flat DNS (Single External Answer) Split-Horizon DNS (Internal vs External) Delta
DNS Resolution Time (p95) 38 ms 11 ms -71% faster internal
HTTP TTFB to assets.example.com 92 ms (via CDN edge + origin fetch) 18 ms (direct to origin LB) -80% reduction
Monthly Egress via NAT / CDN (per 10M internal reqs) ~2.4 TB ~0.3 TB -87% egress cost
Failed Internal Health Checks (per week) 12-15 (hair-pin timeouts) 0-1 -93% fewer false alerts
Zone Management Overhead 1 zone file 2 zone files + ACLs +1 file to maintain

Putting It Into Practice: BIND Views, PowerDNS, and Keeping Zones in Sync

You don't need exotic software to pull this off. Most of us are still doing split-horizon with BIND's views or PowerDNS with Lua records, and honestly, that's more than enough if you automate it correctly. The core idea is simple: your nameserver checks the source IP of the query and serves a completely different zone file based on that match. Internal clients — your office ranges, your VPCs, your VPN — get RFC1918 or direct edge addresses. Everyone else gets the public anycast IPs.

Where teams get burned isn't the config itself, it's drift. I've seen CDNs where the internal zone had an A record for origin.cdn.example.com pointing to a decommissioned node for 9 months because someone updated the external zone via API and forgot the internal file. If you're not generating both zones from a single source of truth, you'll diverge. We keep a 75-80% cache hit rate on our external edge, but internal split-horizon zones often run with much lower TTLs — think 30-60 seconds internally versus 300 seconds externally — so stale records will bite you fast. Keep your TTLs intentional and your sync automated.

// named.conf - BIND9 Split-Horizon with TSIG-secured transfers
acl "internal-nets" {
    10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16;
    2001:db8:ff::/48; // corporate IPv6
};

view "internal" {
    match-clients { internal-nets; key "internal-tsig-key"; };
    recursion yes;
    
    zone "cdn.example.com" {
        type master;
        file "/etc/bind/zones/db.cdn.example.com.internal";
        allow-transfer { key "internal-tsig-key"; };
    };
    // Serve private origin directly, bypassing public WAF edge
    zone "origin.cdn.example.com" {
        type master;
        file "/etc/bind/zones/db.origin.internal";
    };
};

view "external" {
    match-clients { any; };
    recursion no; // authoritative only
    
    zone "cdn.example.com" {
        type master;
        file "/etc/bind/zones/db.cdn.example.com.external";
        allow-transfer { key "external-tsig-key"; };
        also-notify { 203.0.113.53 key "external-tsig-key"; };
    };
};

Advanced Patterns, Cache Poisoning Pitfalls, and Troubleshooting at Scale

Once you've got basic views working, the next headache is EDNS Client Subnet (ECS) and anycast interaction. If your external authoritative servers respect ECS, you're effectively creating thousands of micro-views based on client subnet, which can explode your cache cardinality by 10-15x. For split-horizon, I generally recommend stripping or normalizing ECS on your internal view — your internal resolvers already know the client's /24, you don't need Google's resolver telling you where the user is. And if you're running DNSSEC Signing for CDN Infrastructure: Authenticated DNS Without the Performance Hit, remember you'll be signing two different zone contents with the same zone name. That means separate ZSKs and RRSIGs per view, and you must ensure your key rollover is orchestrated for both views simultaneously or validation will fail for one audience.

Troubleshooting split-horizon is all about seeing which view you actually hit. Don't just dig cdn.example.com from your laptop and assume you're testing the external view. You aren't. Use dig @ns1.cdn.example.com cdn.example.com +short +subnet=0.0.0.0/0 and dig +trace from both an external VPS and an internal jump host. The most common failure we see is a “view leak” where the internal ACL is ordered after the external view — BIND evaluates views top-down, first match wins. If your any view is first, everyone gets the external answer. We also add synthetic logging: a TXT record like view-id.cdn.example.com that returns “internal” or “external” so our synthetic monitors can instantly tell if routing flopped.

Finally, don't treat split-horizon as a security boundary by itself. It's traffic steering, not a firewall. A determined actor can still query your internal nameserver if it's reachable. Pair it with proper hardening. We rate-limit external views to 20-30 responses per second per /24 to blunt enumeration, which ties directly into your DNS Amplification Attack Mitigation: Protecting CDN DNS Infrastructure strategy, and we use internal-only DNS sinkholing to quarantine suspicious origins without affecting public resolution. That layered approach — described in our DNS Sinkholing at CDN Scale: Redirecting Malicious Traffic for Analysis guide — lets you redirect malware CnC lookups to a scrubbing IP only for infected internal hosts while external users see a clean NXDOMAIN.

Frequently Asked Questions

Should my internal and external zones use the same TTL?

Almost never. We run external TTLs at 300 seconds to maximize edge cache hits and reduce authoritative QPS by ~60%, but internal TTLs at 30-60 seconds. That lower internal TTL lets you fail over an origin or edge node in under a minute for employees and staging systems without waiting for thousands of ISP resolvers to expire their cache. Just be ready for 5-8x more query volume on your internal resolvers.

How do I handle split-horizon with DNSSEC?

You have to sign each view independently. The zone name is identical but the RRsets are different, so the RRSIGs will be different. Use inline-signing per view in BIND or separate cryptokeys in PowerDNS. Automate resigning and rollover together — if you roll your ZSK on the external view and forget the internal one, internal validators using the same trust anchor will start seeing Bogus responses. Keep your DS at the parent in sync with both views' KSKs.

Can I use split-horizon to bypass my CDN/WAF for internal traffic?

Yes, and it's one of the best use cases. Point internal clients directly at origin or a private edge PoP (e.g., 10.20.30.5) while external users hit your anycast WAF IPs. You'll shave 15-40ms off internal API calls and avoid paying for WAF inspection on trusted traffic. Just ensure your origin still authenticates the request — don't rely on “only internal DNS knows the IP” as security. Use mTLS or header tokens between your internal resolvers and origin.

Why are some internal clients still getting external answers?

It's 90% ACL or view ordering. First, check that the client's source IP as seen by the nameserver is actually in your ACL — NAT gateways and VPN concentrators often masquerade as a single public IP. Second, confirm BIND's view order: the most specific match must come first, with the “any” external view last. Run rndc status and check query logs with querylog yes to see which view matched. If you're forwarding through a corporate resolver like Infoblox, it may be caching the external response.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker