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
  1. The beacon loads the measurement targets for your service domain.
  2. It fetch-times each member's rumUrl (mode: no-cors, cache-busted), two seconds after page load so it never competes with your assets.
  3. Results are reported with navigator.sendBeacon — non-blocking, fire-and-forget.
  4. Nexus folds samples into an exponentially-weighted moving average keyed by the visitor's subnet (/24 IPv4, /48 IPv6 — the same privacy-safe prefixes used for EDNS Client Subnet) and mirrors the smoothed values to all cluster nodes via NATS KV (gslb_<cluster>_rum bucket). Without NATS, RUM still works node-locally.
  5. At query time the rum step 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/__rum

Members 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:

  1. RUM data for the client subnet covering ≥1 candidate → lowest measured latency wins.
  2. No RUM coverage → probe-RTT latency selection (identical to the latency step, using cluster-wide global-min RTT when available).
  3. 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 rumUrl configured — 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.trustedProxies to the proxy's CIDR(s) so the real client IP is read from X-Forwarded-For — otherwise every visitor collapses into the proxy's single subnet and RUM cannot distinguish them.

Observability

gslbctl rum status

GET /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.


Was this article helpful?
© 2026