Latency Algorithm
Latency-Based Routing
How It Works
The latency algorithm routes DNS queries using three
signals:
- EDNS0 Client Subnet — If your resolver forwards the client subnet (RFC 7871), Nexus extracts a /24 (IPv4) or /48 (IPv6) prefix and maps it to a country via MaxMind GeoLite2.
- GeoIP Country Lookup — Client country is matched against endpoint regions.
- Node RTT — Health probes measure TCP/HTTP round-trip time from each cluster node. A rolling average of the last 5 probes smooths noisy data.
These combine into a weighted selection:
combined_score = region_bonus + (1 / avg_rtt_ns)
Endpoints in the client's country get a region bonus; faster endpoints (lower RTT) get higher selection probability. Traffic is distributed by weighted random selection, not always sent to the single fastest endpoint.
Configuration
loadBalancer:
algorithm: "latency"
latency:
geoipDBPath: "/var/lib/nexus/GeoLite2-Country.mmdb"
regionBonusMultiplier: 2.0 # region match weighs 2x RTT weight
minRTTThresholdMs: 5 # don't shift for <5ms differences
fallbackRegion: "unknown"| Field | Type | Default | Description |
|---|---|---|---|
geoipDBPath |
string | "" (falls back to geoip.dbPath) |
Path to MaxMind .mmdb file |
regionBonusMultiplier |
float | 1.0 |
Multiplier for same-region bonus |
minRTTThresholdMs |
int | 5 |
Hysteresis floor in milliseconds |
fallbackRegion |
string | "unknown" |
Region label when GeoIP unavailable |
Endpoint Regions
Each member has a region field set when creating or
updating via the API:
POST /api/v1/pools/{id}/members
{
"ipAddress": "10.0.1.5",
"port": 80,
"region": "US"
}Region codes match the ISO 3166-1 alpha-2 country codes returned by
the GeoIP database (e.g. "US", "DE",
"JP"). Free-form values are also supported — the match is
an exact string comparison, so be consistent.
EDNS0 Support
EDNS0 CLIENT_SUBNET is opt-in and depends on your recursive resolver configuration.
| Resolver | Forwards ECS | Notes |
|---|---|---|
| Enterprise DNS (BIND, Knot, PowerDNS) | ✅ Yes | Most DNS operators enable by default |
| Cloudflare Resolver (1.1.1.1) | ❌ No | Strips for privacy |
| Google Public DNS (8.8.8.8) | ❌ No | Strips for privacy |
| Quad9 | ❌ Mostly no | Privacy-focused |
When ECS is not available, Nexus uses
remoteIP(w.RemoteAddr()) — the resolver's IP — for GeoIP
lookup. For public resolvers that strip ECS, the region bonus is applied
based on where the resolver is, not the client.
Hysteresis
To prevent rapid endpoint switching when RTT fluctuates slightly:
- RTT differences below
minRTTThresholdMsdo not trigger a traffic shift - Rolling average of last 5 probes smooths single-probe spikes
- Traffic can still shift when a region-match bonus outweighs a small RTT penalty
Fallback Chain
| Signal | Unavailable | Fallback |
|---|---|---|
| EDNS0 client subnet | No ECS option in request | Use resolver IP from RemoteAddr() |
| GeoIP lookup | DB not loaded or IP not found | client_region = fallbackRegion → no region bonus |
| Node RTT | Fewer than 3 probes collected | Endpoint excluded from weighted selection |
| All RTT data | No probes for any candidate | Return first healthy candidate |
Distributed RTT (Multi-Node Aggregation)
In a multi-node cluster each gslbd node probes backends
independently. With distributed RTT enabled, nodes share their probe
results via NATS KV and the latency algorithm uses the
global minimum RTT across all nodes rather than only
the local node's measurement.
How it works
- Each publisher writes its per-endpoint RTT to the NATS KV bucket
gslb_{clusterID}_rttunder the key{nodeID}/{memberIP}. - Each subscriber watches the same bucket and feeds a local
RTTStorecache:ip → nodeID → {rttMs, ts}. latencyWeighted()callsRTTStore.GlobalMinRTT(ip)which returns the minimum RTT across all nodes whose data is less than 3 minutes old. Falls back to localAvgRTT()when the store has no data.
Requirements
NATS JetStream must be enabled (it is required for cluster state sync anyway). The RTT KV bucket is created automatically with a 5× health-interval TTL on each node that connects.
API and WebUI
GET /api/v1/pools/{id}/status returns both fields per
member:
{
"10.0.1.5": {
"healthy": true,
"score": 1.0,
"rttMs": 4.2, // local probe RTT from this node
"globalRttMs": 2.1 // minimum across all cluster nodes
}
}The WebUI pool detail shows globalRttMs when available;
hovers over it say "Global min RTT across cluster". When distributed RTT
is disabled only rttMs is present.
Staleness handling
RTT entries older than 3 minutes are excluded from aggregation. If
all entries for an IP are stale the store returns
(0, false) and the local probe RTT is used as fallback.
Limitations
- RTT is measured from cluster nodes to endpoints, not from clients. For clients far from all cluster nodes, the measured RTT may not reflect client-perceived latency.
- EDNS0 is stripped by major public resolvers — for those clients, geography-based routing uses the resolver's IP, not the client's actual location.
- The algorithm requires at least 3 successful health probes per endpoint before RTT data is used. During startup, traffic is routed round-robin until sufficient data is collected.