Running a single region behind a well-tuned CDN will cover you until the day it doesn't — a regional cloud outage, a fiber cut into an entire AZ, or a bad deploy that takes out your primary PoP's origin path. That's when Global Server Load Balancing stops being an architecture diagram and becomes your incident response. In my experience running multi-region active-active footprints for media and commerce platforms, GSLB is less about picking a vendor's algorithm and more about understanding how DNS, BGP, and your application state interact under failure. Get the steering logic right and users fail over in seconds without noticing; get it wrong and you flap traffic, poison caches, or pin users to a region that can no longer serve them. This article breaks down the steering models, health-checking patterns, and consistency tradeoffs I rely on when designing GSLB for production multi region deployments where every second of misrouting has a revenue cost.
Why DNS-Based GSLB Remains Unavoidable for Multi-Region Deployments
Despite the rise of anycast and layer 7 global load balancers, DNS is still the only control plane that can steer traffic before a TCP handshake ever happens. When a client resolves api.example.com, the authoritative GSLB nameserver is your first and often only chance to decide which region handles that connection. That decision happens outside your data plane, which is both its strength and its weakness — it requires no proxy hop, but it is bound by caching and the client's resolver behavior.
I've found that teams new to global load balancing underestimate how much EDNS Client Subnet (ECS) changes this model. Without ECS, your GSLB sees only the resolver's IP, often hundreds of miles from the actual user. With ECS, you see a truncated client prefix and can make far more accurate proximity decisions. Most major public resolvers — Google Public DNS, Cloudflare 1.1.1.1, OpenDNS — now send ECS, but many ISP and corporate resolvers don't. Your steering logic must gracefully handle both cases, and you should measure what percentage of your queries actually include ECS before you trust a geo-based policy.
The TTL Tradeoff Nobody Wants to Talk About
Every GSLB design lives or dies on TTL. A 5-second TTL gives you fast failover, but it multiplies your authoritative query volume by 60x compared to a 300-second TTL and puts more pressure on resolvers. In my experience, a 20-30 second TTL for apex A/AAAA answers used for GSLB is the pragmatic middle ground for most internet-facing APIs. It keeps re-resolution fast enough to drain a failed region within a minute, while keeping QPS and cost manageable. For static asset hostnames fronted by a CDN, you can be more aggressive — I've run 10-second TTLs there without issue because the CDN's own edge caches shield the origin.
Record Types and Why You Should Prefer Address Answers
Some architectures try to implement GSLB with CNAME chains per region (us-east.example.net, eu-west.example.net). That adds an extra lookup and hurts connection setup. Whenever possible, answer directly with A and AAAA records from the GSLB zone. If you need region-specific names for debugging or certificate isolation, use them as hidden targets that your monitoring hits directly, but keep the public hostname as a direct address answer. This also plays better with DNS Prefetching and Preconnect: Reducing Connection Setup Latency optimizations because the browser can preconnect without resolving an intermediate CNAME.
Proximity Routing vs. Latency-Based Steering: Measuring What Actually Matters
Proximity sounds intuitive — send the user to the nearest region geographically. Latency-based routing sounds better — send the user to the region with the lowest round-trip time. In practice, they are not the same, and I've seen proximity fail badly when backbone routing doesn't follow geography.
A user in Lisbon may be 600km from Madrid but have 15ms lower RTT to London because of submarine cable paths and peering. Similarly, a user in São Paulo might be geographically close to a new edge in Santiago but still faster to reach Miami due to capacity on that path. Pure GeoIP steering based on MaxMind or similar databases will encode these errors into your policy.
Latency-based steering requires active measurement. Providers like Route 53, NS1, and Dyn collect latency probes from resolver vantage points or use synthetic agents that measure TCP connect time to your regional load balancers. You publish latency tables, and the GSLB uses them to map ECS prefixes to regions. I've found that this works well once you have sufficient probe volume, but it takes days to converge after you add a new region. During that convergence window, you will see uneven distribution.
For most multi region setups, I recommend starting with a hybrid: geo as the default with latency as an override for your top 20 prefixes by traffic. That keeps the config explainable while capturing most of the performance benefit.
| Steering Method | Data Source | Failover Speed | Best For | Common Pitfall |
|---|---|---|---|---|
| Geographic Proximity | GeoIP database + ECS prefix | Dependent on TTL (20-60s) | Compliance, language pinning, predictable routing | Mis-steering where network distance != geography |
| Latency / Performance | Active probes, RUM data, synthetic checks | Dependent on TTL + probe interval | Throughput-sensitive APIs and media delivery | Slow convergence; noisy data for long-tail prefixes |
| Weighted / Capacity | Manual weights or autoscaled capacity signals | Immediate on weight change | Progressive rollouts, regional overflows | Overloading a region when weights don't reflect real capacity |
| Geofencing / Topology | ECS + client prefix ACLs | Dependent on TTL | Data residency, embargoed countries, licensed content | Resolvers without ECS fall back to resolver geo and bypass fencing |
| Anycast + Local GSLB | BGP announcement, local load balancer health | Sub-second (BGP withdraw) | TCP/UDP apps needing instant failover | Troubleshooting reachability when routing is opaque |
Feeding Real User Measurements Back Into Steering
The most accurate latency steering I've built didn't rely solely on synthetic probes. We emitted navigation timing data from the client — connectEnd - connectStart and server timing headers — and aggregated it by /24 and by region served. That RUM data was fed nightly into our GSLB weight generator. The specifications for Server-Timing and Resource Timing are detailed in the HTTP Working Group Specifications, and they provide a clean, standards-based way to expose server-side handling time without custom headers. Correlating server timing with client-measured RTT made it obvious when a region looked healthy but was silently degraded.
Health Checking at Global Scale: Synthetics, Origin Signals, and Failover Thresholds
A GSLB is only as good as its health checks. If your checks are too sensitive, you flap traffic on transient blips. If they are too lenient, you keep sending users to a region that is returning 500s. I standardize on a three-tier model.
Tier 1 is edge synthetic: HTTP checks from 6-12 distributed probers that resolve your GSLB name and then establish a full TLS connection to the regional VIP. They request a lightweight /healthz that executes a deep check — database read, cache read, and downstream dependency status. Tier 2 is regional load balancer health: your in-region L4/L7 LB's own health state for its pool. Tier 3 is origin signal: error rates and latency percentiles as reported by the CDN or API gateway in each region. Tier 1 is what drives DNS failover; Tiers 2 and 3 are what prevent a region from being reintroduced too early.
Thresholds matter more than frequency. I've settled on requiring 3 consecutive failures from at least 2 vantage points within 30 seconds before marking a region down. For recovery, require 5 consecutive successes over 60 seconds. This hysteresis prevents the classic flapping loop where a struggling origin recovers enough to pass one check, gets traffic back, and collapses again.
# Example: distributed health check definition (Terraform for NS1-style GSLB)
# This is illustrative pseudocode - adapt to your provider's schema
resource "ns1_monitor" "api_eu_health" {
name = "api-eu-deep-health"
url = "https://api-eu.example.com/healthz"
method = "GET"
interval = 10 # seconds between probes
timeout = 5
regions = ["ams", "lhr", "fra", "iad", "sin"]
check_type = "http"
expected_codes = [200]
# Body must contain "status":"ok" and db_ok:true
expected_body_regex = "\"status\"\\s*:\\s*\"ok\".*\"db_ok\"\\s*:\\s*true"
notify_list {
id = ns1_notify_slack_incidents.id
}
}
resource "ns1_record" "api_gslb" {
zone = "example.com"
domain = "api.example.com"
type = "A"
# Latency + failover policy
answers {
answer = "203.0.113.45" # eu-west VIP
meta = {
up = "${ns1_monitor.api_eu_health.up}"
georegion = "EU"
latency_zone = "eu-west"
}
}
answers {
answer = "198.51.100.22" # us-east VIP
meta = {
up = "${ns1_monitor.api_us_health.up}"
georegion = "US-EAST"
latency_zone = "us-east"
}
}
# Filter chain: first remove down answers, then pick lowest latency
filters = ["select_first_n", "up", "priority_steering"]
}
Origin-Aware Failover Without Cascading
When a region fails health checks, the GSLB removes its addresses. What it shouldn't do is immediately shift 100% of that region's traffic to the next closest region if that region is already at 75% capacity. I configure ordered failover targets plus capacity caps. For example, EU-West fails over to US-East up to +30% over baseline, then spills to AP-Southeast. This requires your GSLB to support either weighted failover or a capacity-aware filter chain. If yours doesn't, you can approximate it with low TTLs and an external controller that rewrites weights based on capacity metrics from Prometheus or CloudWatch. The details on safe retry and failure signaling across HTTP are covered in the retry semantics drafts on IETF Datatracker, which I recommend reviewing before you build custom failover on top of 503s.
For a deeper look at keeping origins viable during these shifts, see CDN Failover and Origin Redundancy: Ensuring Availability During Outages — the origin shielding and stale-serve patterns there complement GSLB-level decisions.
Weighted and Geofenced Steering for Regulatory and Capacity Constraints
Not every multi region decision is about performance. I've run deployments where German users had to stay inside the EU for data residency, where Brazilian traffic had to avoid a region with limited payment processor peering, and where a new region in Jakarta needed only 5% of traffic for a soak test. GSLB has to encode these business rules without turning into an unmaintainable mess of ACLs.
Geofencing in GSLB is implemented as a match on ECS / country / ASN with a forced answer set. The sharp edge: requests from resolvers that don't send ECS will be mapped by the resolver's location, not the client's. In practice this means a non-ECS resolver in the US that serves a user in Germany may get a US answer and violate your fencing intent. The mitigation is to also enforce residency at the edge — return a 451 or redirect if the edge detects a mismatch between CF-IPCountry or equivalent and the region serving the request. Don't rely on DNS fencing alone for compliance.
Weighted steering is the safest way to introduce a new region or drain one for maintenance. Treat weights as a percentage of queries that should receive that region's answers when all other filters have passed. I've found that starting at 1% for 24 hours, then 5%, 25%, 50% catches configuration errors like missing certificates or incomplete database replication before they become incidents. Pair weighted steering with your progressive delivery tooling; the pattern is nearly identical to what we use for Traffic Splitting for Progressive Rollouts: Weighted Routing at the CDN Layer, just applied at the DNS layer instead of the CDN edge.
// PowerDNS / dnsdist-style Lua for geofenced + weighted GSLB
-- Simplified logic for api.example.com
-- Assumes ECS is available; falls back to resolver IP
function gslbDecision(dq)
local ecs = dq:getEDNSSubnet() or dq.remoteaddr
local country = geoipCountry(ecs)
local weights = getCurrentWeights() -- fetched from control plane KV
-- 1. Enforce residency: DE/FR users must stay in EU
if country == "DE" or country == "FR" then
if isHealthy("eu-west") then
return pickWeighted({"eu-west"}, weights)
else
-- Do not fail over outside EU automatically; return EU with degraded status
-- Edge will enforce fallback behavior with a 451 + retry header
return "203.0.113.45" -- eu-west VIP, still returned but flagged
end
end
-- 2. Soak test for new ap-southeast region
if isHealthy("ap-southeast") and weights["ap-southeast"] > 0 then
if weightedChoice(weights) == "ap-southeast" then
return "192.0.2.88" -- ap-southeast VIP
end
end
-- 3. Normal latency-based selection for everyone else
return selectByLatency(ecs, {"us-east", "eu-west", "ap-southeast"})
end
Capacity Signals Instead of Static Weights
Static weights drift out of sync with real capacity, especially if you autoscale on CPU or request queue depth. A better model is to publish a capacity score per region every 15-30 seconds — something like capacity_score = 1 - (p95_cpu / 100) blended with available connection slots — and have your external GSLB controller translate that into weights. I keep a floor of 10% weight for any healthy region to avoid cold starts; pulling a region to 0% in DNS and then suddenly restoring it creates a thundering herd that will test your autoscaler in the worst way.
Anycast Front Doors Compared to DNS GSLB: When to Use Which
Anycast and DNS GSLB solve the same problem at different layers, and most large platforms use both. With anycast, you announce the same IP prefix from multiple regions and let BGP's shortest path decide where the packet lands. Failover is BGP withdrawal — typically sub-second to a few seconds — and there is no TTL caching issue. The tradeoff is loss of control: you don't get to decide that a user in Berlin should go to Amsterdam instead of Frankfurt because BGP path length may prefer Frankfurt even if Amsterdam has lower latency that day.
In my experience, anycast is unbeatable for stateless edge services: DNS itself, TLS termination for static assets, volumetric DDoS absorption, and simple API gateways that don't need sticky sessions. It also hides regional failures well because withdrawal automatically pulls traffic away without any client-side re-resolution. For a full explanation of how this routing works under the hood, Anycast DNS Architecture: How CDNs Route Users to the Nearest Edge is a useful reference.
DNS GSLB gives you explicit control, per-query visibility, and the ability to encode business logic that BGP can't express. You can implement blue-green regional promotes, data residency, ASN-based steering for a large ISP that has poor peering to one region, or time-of-day shifting to chase lower carbon energy. The cost is complexity and the reality that some clients will cache beyond your TTL.
My default architecture now is a hybrid: anycast for the CDN edge and for the authoritative DNS tier itself, DNS GSLB for origin selection behind that edge. In practical terms, www.example.com resolves via GSLB to a regional anycast VIP that then routes via the CDN's internal fabric to the optimal origin. If you run your own authoritative DNS, ensure it is itself anycasted across at least three sites; otherwise your GSLB control plane becomes a single point of failure.
TTL + Anycast Interaction to Watch For
One subtle failure mode I've debugged twice: a GSLB returns an anycast VIP for Region A, the client caches it, then Region A withdraws the anycast route. The client still has the same IP cached, but now BGP routes that IP to Region B. If Regions A and B don't share the same TLS session ticket keys and session state, the resumed connection fails in a way that looks like an intermittent TLS error. Solve this by synchronizing session tickets and by using a shared session store or TLS 1.3 with PSK resumption that doesn't depend on regional affinity.
Consistency and Session Affinity Across Regions Without Breaking Failover
The hardest part of GSLB isn't routing — it's what happens after the packet arrives. Global load balancing makes regional failover invisible at the DNS layer, but your application may still have strong consistency requirements that make active-active hazardous.
If your workload is read-heavy and can tolerate eventual consistency, active-active behind GSLB works well. Put a write-primary in one region, replicate asynchronously to secondaries, and steer reads to the closest region. Users who write and then immediately read may see stale data in another region, but you can mitigate that with a read-your-writes cookie that pins the user to the write region for 10-20 seconds after a mutation. I've used a simple edge-set cookie for this: after a POST, set __gslb_pin=eu-west; Max-Age=20; Secure and have the GSLB respect that cookie via a mapping to a region-specific hostname on the next navigation. It's not perfect — DNS doesn't see cookies — so implement the pin at the CDN edge or in the app's fetch logic rather than expecting DNS to honor it.
For workloads that require strong consistency — financial ledger entries, inventory decrement, collaborative editing — avoid multi-writer active-active. Run active-passive at the data layer and use GSLB only for read steering and for fast promotion of the passive region when the primary fails. Your health check must then incorporate replication lag: if lag exceeds your RPO (say, 5 seconds), don't promote automatically. I've seen automated promos with 30 seconds of lag cause duplicate order processing that took weeks to reconcile.
# Edge logic to honor a short pin after writes (VCL-style pseudocode)
# Runs at the CDN edge before origin selection, not in DNS
sub vcl_recv {
# If client recently wrote in eu-west, prefer eu-west origin
if (req.http.Cookie ~ "__gslb_pin=eu-west" && health.eu_west == "healthy") {
set req.backend_hint = origin_eu_west;
# Also add a header so origin can enforce read-your-writes
set req.http.X-GSLB-Pin = "eu-west";
} else if (req.http.Cookie ~ "__gslb_pin=us-east" && health.us_east == "healthy") {
set req.backend_hint = origin_us_east;
set req.http.X-GSLB-Pin = "us-east";
} else {
# Fall back to GSLB DNS decision already made at resolve time
# Use latency-based origin selection
set req.backend_hint = origin_closest;
}
# On POST/PUT/PATCH responses, set the pin cookie
}
sub vcl_deliver {
if (req.method ~ "POST|PUT|PATCH" && resp.status ~ "^2") {
# Pin for 20 seconds; edge will honor this before next DNS lookup
add resp.http.Set-Cookie = "__gslb_pin=" + regsub(req.backend_hint, "origin_", "") + "; Path=/; Max-Age=20; Secure; SameSite=None";
add resp.http.Cache-Control = "private, no-store";
}
}
Testing Failover Without Taking Down Production
You can't trust a GSLB runbook you haven't exercised. My team runs quarterly failover drills with three techniques that don't require an actual outage. First, synthetic brownouts: we publish a GSLB response that returns only one region for a canary hostname (gslb-test.example.com) and validate that synthetics correctly follow. Second, controlled degredation: we mark a region's health check as failed via a feature flag, so GSLB drains it while the region is still healthy enough to handle in-flight connections. Third, chaos at the origin: we inject latency or 5xx at the regional LB for a percentage of traffic and watch that GSLB health checks trigger but don't flap. Log every decision — ECS prefix seen, answers returned, health state at decision time — into BigQuery or ClickHouse. When an incident does happen, you will need that log to answer why a specific user went to the wrong region.
Observability You Actually Need
Most GSLB dashboards show QPS and health status. That's not enough. I monitor: percentage of queries with ECS, cache hit rate at major resolvers (inferred from TTL behavior), per-region p50/p95 time-to-first-byte by client prefix, failover event frequency, and weight vs. capacity divergence. Alert when ECS coverage drops below 70% or when a region's traffic share deviates more than 15% from its configured weight — both are early signs of a steering or capacity issue that will become user-visible.
Frequently Asked Questions
How low should my GSLB TTL be for fast failover without overloading authoritative DNS?
For internet-facing GSLB records that steer origin traffic, 20-30 seconds is a solid default. It drains a failed region in roughly TTL + health check interval + propagation, often 45-60 seconds total, while keeping query volume manageable. You can go to 10 seconds for hostnames behind a CDN that shields origin, but below 10 seconds most public resolvers will clamp your TTL to their minimums anyway, so you pay more without failing over faster. Always measure resolver compliance with your TTL before tuning lower.
Can GSLB guarantee users stay in a specific country for data residency?
No — DNS alone cannot guarantee residency because clients or resolvers may cache answers, and resolvers without EDNS Client Subnet are mapped by resolver location rather than client location. Use GSLB geofencing as a best-effort steering layer, then enforce residency at the anycast edge or application layer where you can inspect client IP, TLS ClientHello, or a residency token and return a 451 or route to a compliant region if the check fails. Audit both layers for coverage.
Should I use DNS GSLB or anycast for my global load balancer?
Use anycast where you want sub-second, BGP-driven failover and don't need per-query business logic — typically for edge TLS termination, DNS, and DDoS mitigation. Use DNS GSLB where you need explicit control over weights, geography, latency-based decisions, or compliance routing. Most mature platforms combine them: anycast for the edge and for the authoritative DNS tier itself, DNS GSLB for selecting which regional origin or stack handles the request behind that edge.
How do I handle session affinity when GSLB can move a user between regions?
Avoid relying on DNS to hold affinity; it doesn't see cookies or application state and answers are cached beyond your control. Keep affinity at the edge or in the client: set a short-lived pin cookie after writes (10-20 seconds) and honor it in your CDN or API gateway's origin selection, or use a shared session store like Redis or a global database so any region can serve any user. If you must pin longer, pin to a region-specific hostname (e.g., eu.api.example.com) rather than trying to make the apex GSLB sticky.