RUM Routing Reference
License: pro tier and above.
RUM routing steers DNS answers using latency measured from real visitors' browsers, not from cluster-node health probes. A small JavaScript beacon on your site fetch-times each pool member; Nexus aggregates the results per client subnet and the rum filter-chain step answers each query with the member that is actually fastest for that client's network — including last-mile, peering, and CDN effects that node-side probes can never see.
How It Works
visitor browser ──(1) load /rum.js──────────▶ Nexus API
◀─(2) GET /api/v1/rum/config── targets: member rumUrls
──(3) fetch-time each rumUrl──▶ member endpoints
──(4) sendBeacon samples──────▶ POST /api/v1/rum/ingest
│
EWMA per (client /24 or /48, member IP)
│
NATS KV mirror → all cluster nodes
│
DNS query (ECS or resolver IP) ──▶ "rum" step ──▶ lowest-latency member
- The beacon loads the measurement targets for your service domain.
- It fetch-times each member's
rumUrl(mode: no-cors, cache-busted), two seconds after page load so it never competes with your assets. - Results are reported with
navigator.sendBeacon— non-blocking, fire-and-forget. - Nexus folds samples into an exponentially-weighted moving average keyed by the visitor's subnet (
/24IPv4,/48IPv6 — the same privacy-safe prefixes used for EDNS Client Subnet) and mirrors the smoothed values to all cluster nodes via NATS KV (gslb_<cluster>_rumbucket). Without NATS, RUM still works node-locally. - At query time the
rumstep matches the client subnet (from ECS when the resolver forwards it, otherwise the resolver address) against the measurement map and picks the lowest-latency candidate.
Measurements go stale after 30 minutes and are then ignored; a subnet with no fresh data falls back cleanly (see below).
Setup
1. Give each member a measurement URL — any small, always-on http(s) endpoint served by that member (a 204 handler, a 1-pixel asset, a health route). Set it via WebUI (member panel → RUM URL), API (rumUrl on member create/update), or CLI:
gslbctl members update <member-id> --name web-01 --rum-url https://web-01.example.com/__rumMembers without a rumUrl are never measured and never win the rum step — they still participate in fallback selection.
2. Embed the beacon on any page served to your users:
<script src="https://<nexus-api-host>/rum.js" data-domain="app.example.com" async></script>data-domain is the Nexus service whose pool should be measured (defaults to location.hostname).
3. Add the rum step to the service's filter chain:
["geo-ip", "rum"]rum (alias rum-latency) is a terminal selector: it picks exactly one candidate and ends the chain.
Fallback Ladder
The step never fails a query — it degrades in order:
- RUM data for the client subnet covering ≥1 candidate → lowest measured latency wins.
- No RUM coverage → probe-RTT latency selection (identical to the
latencystep, using cluster-wide global-min RTT when available). - No RTT data at all → first candidate in current (health-scored) order.
Unhealthy members are filtered out before the chain runs, so RUM can never route to a down member regardless of how fast it once measured.
Ingest Endpoint Hardening
POST /api/v1/rum/ingest and GET /api/v1/rum/config are unauthenticated by necessity (they're called from arbitrary visitors' browsers, cross-origin). Defences:
- Samples are only accepted for member IPs that belong to the claimed domain's pool and have a
rumUrlconfigured — arbitrary IPs cannot be injected into routing decisions. - Latency values must be in
(0, 60000]ms; the body is capped at 8 KB / 32 samples. - Per-source-IP token bucket: 2 requests/second, burst 10. At its IP cap the limiter evicts only fully-refilled (idle) buckets — a distinct-IP flood can never reset established clients' limits or re-grant itself a full burst.
- A poisoned or noisy source only influences routing for its own /24 (or /48) — the blast radius of junk data is the attacker's own subnet.
- The store is capped at 50,000 subnets; entries expire after 30 minutes.
Behind a reverse proxy: the source IP used for both rate limiting and subnet attribution comes from the connection address by default. If gslbd sits behind a proxy (Caddy, nginx, a load balancer), set
api.trustedProxiesto the proxy's CIDR(s) so the real client IP is read fromX-Forwarded-For— otherwise every visitor collapses into the proxy's single subnet and RUM cannot distinguish them.
Observability
gslbctl rum statusGET /api/v1/rum/status (requires pools:read) returns fresh-data coverage: tracked subnets, total measurements, and per-member subnet counts.
The query tracer (gslbctl trace, topology Trace panel, POST /api/v1/simulate) shows the rum step's decision, including whether it selected on real-user data or fell back to probe RTT.
Comparison with the latency Step
latency |
rum |
|
|---|---|---|
| Measures | node → member RTT (health probes) | visitor browser → member fetch time |
| Vantage | your cluster nodes | your actual users |
| Sees last-mile/peering effects | no | yes |
| Needs client-side integration | no | yes (one script tag) |
| Data freshness window | ~minutes (probe interval) | 30 min EWMA |
Use rum with latency semantics as its built-in fallback: chains like ["rum"] or ["geo-ip", "rum"] are the recommended forms.