Filter chains let you compose multiple routing steps per DNS query. Instead of locking a service to a single algorithm, you declare an ordered list of filters that are applied in sequence — narrowing the candidate set at each step until a terminal filter selects one endpoint.
License: Filter chains require the paid tier or above. Services with a non-empty
filterChainarray return HTTP 403 on free and unlicensed installations. Contact licensing@gslb.nexus to upgrade.
Filter Categories
Filters fall into two categories.
Narrowing filters
A narrowing filter reorders or reduces the candidate set. If applying the filter would produce zero candidates, it is skipped (pass-through) and the previous candidate set is kept unchanged. The chain then continues with the next filter.
Filter | Behaviour |
|---|---|
| Keeps only candidates whose |
| Reorders candidates by ASN proximity using |
| Reorders candidates by CIDR map preference using |
| Pins the client to a single member (LDNS persistence). Narrows to one candidate chosen by rendezvous hashing of the client subnet. See LDNS Persistence. |
| Biases (never removes) candidates by a per-region carbon/cost value. Requires pro tier. See Signal-Weight Routing and CarbonAwareRouting.md. |
Selecting filters (terminal)
A selecting filter picks exactly one candidate and terminates the chain. No further filters run after it.
Filter | Behaviour |
|---|---|
| Stateful round-robin across the current candidate set. Counter is per-service. |
| Weighted random selection from the current candidate set using member |
| Returns the highest-priority healthy candidate (lowest |
| Returns a candidate by weighted random selection, weighted by inverse RTT measured from this cluster node. |
| Returns the candidate with the lowest real-user latency measured from the client's subnet by the browser beacon. Falls back to |
Execution Model
Given a chain ["geo-ip", "asn", "weighted-round-robin"]:
Start with all healthy candidates for the pool, sorted by health score.
geo-ip: narrow to candidates in the client's region. If none match, keep all.
asn: reorder survivors by ASN proximity. Keep all survivors.
weighted-round-robin: weighted-random-select one candidate from survivors. Return it.
If the chain exhausts without reaching a terminal filter (e.g. a chain of only narrowing filters), the first candidate in the current set is returned.
Member region Field
The geo-ip filter matches against each member's region field. This must be set explicitly — it is not derived from the member's IP address.
Region values must match the ISO 3166-1 alpha-2 country codes returned by the GeoIP database (e.g. "US", "DE", "JP"). Comparison is case-sensitive. Members with region unset default to "unknown" and will not match any client country.
POST /api/v1/pools/{id}/members{ "ipAddress": "10.0.1.5", "port": 80, "region": "US"}
ConfigurationREST API
Set filterChain on create or update. When filterChain is present, algorithm is ignored.
POST /api/v1/services{ "name": "web-prod", "domain": "www.example.com.", "poolId": "pool-uuid", "filterChain": ["geo-ip", "weighted-round-robin"], "ttl": 30}
PUT /api/v1/services/{id}{ "name": "web-prod", "domain": "www.example.com.", "poolId": "pool-uuid", "filterChain": ["geo-ip", "asn", "failover"], "ttl": 30}
To revert to a single algorithm, send algorithm with an empty or absent filterChain:
PUT /api/v1/services/{id}{ "name": "web-prod", "domain": "www.example.com.", "poolId": "pool-uuid", "algorithm": "round-robin"}
The filterChain field is returned in all service responses when set:
{ "id": "svc-uuid", "name": "web-prod", "domain": "www.example.com.", "poolId": "pool-uuid", "algorithm": "", "filterChain": ["geo-ip", "weighted-round-robin"], "ttl": 30}
Web UI
When creating or editing a service, click Use filter chain (top-right of the routing section) to switch from the single-algorithm dropdown to the filter chain builder:
Add filters from the palette below the chain.
Reorder with the ↑ / ↓ buttons.
Remove individual steps with ✕.
A preview line shows the composed chain:
Geo IP → Weighted Round Robin.
Click Use single algorithm to revert to a single algorithm value.
TUI (gslbctl)
When creating or editing a service, the form includes a Filter Chain (optional) field below the Algorithm selector. Enter filter names as a comma-separated list:
Filter Chain (optional): geo-ip,asn,weighted-round-robin
Leave the field blank to use the value from the Algorithm selector instead. The service list view displays the chain in the ALGORITHM column as geo-ip→asn→weighted-round-robin.
Terraform
Use the filter_chain attribute on nexus_service. When set, algorithm is ignored by the API.
resource "nexus_service" "web" { name = "web-prod" domain = "www.example.com." pool_id = nexus_pool.web.id filter_chain = ["geo-ip", "weighted-round-robin"] ttl = 30}
To use a single algorithm, omit filter_chain and set algorithm:
resource "nexus_service" "web" { name = "web-prod" domain = "www.example.com." pool_id = nexus_pool.web.id algorithm = "round-robin" ttl = 30}
Both algorithm and filter_chain are Optional+Computed. If neither is specified, the service defaults to round-robin.
LDNS Persistence (Sticky)
The sticky step gives a client consistent answers over time — the same member on every query — the way F5 BIG-IP DNS (GTM) per-LDNS persistence does. Use it for stateful backends: sessions pinned to a site, licensing servers, legacy apps that don't share state across members.
How it works. sticky narrows the candidate set to a single member chosen by rendezvous (highest-random-weight) hashing of the client's resolver subnet (/24 for IPv4, /64 for IPv6; ECS subnet when present). Place it before the terminal selector:
["geo-ip", "sticky", "weighted-round-robin"]
Here geo-ip narrows to the client's region, sticky pins to one member within that region, and the selector trivially returns it.
Why rendezvous hashing, not a session table. The pin is a pure function of (client subnet, healthy candidate set):
Deterministic and node-local-free — every node computes the same answer, so a client hitting a different anycast node still gets the same member. No shared affinity table, no cross-node sync.
Survives restarts — there is no state to lose.
Minimal disruption — when a member is added or removed, only the clients pinned to that member move; everyone else keeps their pin. (A modulo or round-robin scheme would reshuffle everyone.)
Health always wins over stickiness. Candidates are health-filtered before the chain runs, so a member that fails its health check simply drops out of the set and its clients re-pin to the next-highest-weight healthy member immediately. There is no "sticky to a dead target" window.
No affinity window in v1. Stickiness lasts exactly as long as the pinned member stays healthy — which is the behaviour stateful backends want — so there is no windowSeconds to tune. The tradeoff: the pin follows the current healthy set, so scaling a pool up/down re-pins the clients whose hash winner changed (see minimal-disruption above), rather than holding an expiring lease.
Migrating from F5 BIG-IP DNS. A GTM pool with "Global Availability" or "Ratio" load balancing plus per-LDNS persistence maps to a filter chain of your selecting step (e.g. weighted-round-robin for Ratio) with sticky in front of it. F5's persistence TTL has no direct equivalent — Nexus persists for the lifetime of the member's health rather than a fixed timer.
Query tracer. The Query Tracer shows the sticky step as a narrowed action pinning to one member (or skipped when ≤1 candidate remains).
Signal-Weight Routing (Carbon/Cost)
Pro tier.
signal-weight biases candidate ordering by a per-region scalar signal — carbon intensity, cost, or any value where lower is preferred — without ever excluding a healthy member. It's a reorder among already-health-filtered candidates, same as asn/map-file.
Members map to a signal zone via their existing region field (the same field geo-ip uses). Configure the zone table server-side, not per-service:
signals: static: us-east: 450 # e.g. gCO2/kWh eu-west: 120 weight: 0.5 # 0 disables the bias; 1 lets it dominate. Default 0.5.
v1 ships static, operator-supplied values only — no live feed integration (ElectricityMaps, WattTime) yet. systemctl reload gslbd (SIGHUP) picks up config changes without a restart.
Blend, not override. Candidates are re-scored as score*(1-weight) + bias*weight, where bias is a min-max normalization of the signal value among the candidates present in this query (lowest value → highest bias). A candidate whose region has no configured value keeps its original score untouched — it's never penalized for missing data, though it can still be fairly outranked by a candidate whose blended score legitimately comes out higher.
Query tracer. Shows a reordered action with the zone values used and the configured weight, or skipped when no candidate's region has a configured value.
Observability. gslbd_signal_zone_value{zone} gauge reports the live configured table; gslbd_signal_queries_steered_total{service} counts queries where the step actually changed the top-ranked candidate. Both are visible in the WebUI under Signal Weight (Infrastructure); gslbctl signals list for the CLI.
See CarbonAwareRouting.md for the full walkthrough and the ESG/marketing framing.
Scriptable Steps (Starlark)
Enterprise tier. Requires the scripts:write permission to author.
A chain step may be an inline Starlark script for custom routing logic that the built-in filters don't cover — route by a customer ID embedded in the qname, business-hours weighting, and similar long-tail rules.
A script step is a chain entry of the form script:<body> (the body is a Starlark program). It runs on the DNS hot path against the current candidate set and narrows/reorders it.
Inputs
The script is given these globals:
Global | Type | Fields |
|---|---|---|
| list of dict |
|
| dict |
|
| string | the queried name |
| string |
|
| int | current unix time |
The script must set the global result to a list of candidate IP strings — the surviving candidates, in the order it wants them. IPs not in the original candidate set are ignored.
Sandbox & safety
No I/O: no file, network, or module (
load) access.Bounded: a step budget plus a ~2 ms wall-clock deadline (scripts run per query).
Fail-open: any compile error, timeout, or a result that filters everything out leaves the candidate set unchanged, increments
gslbd_dns_script_step_error_total{service}, and logs (rate-limited). A broken script never blackholes a service.Validated on write: the body is compile-checked when the service is saved, so a syntax error is rejected by the API — not discovered at query time.
The Query Tracer shows each script step's before/after candidates and run duration.
Examples (cookbook)
Business-hours weighting — only keep the primary region during 09:00–17:00 UTC, otherwise keep all:
hour = (now // 3600) % 24if 9 <= hour < 17: result = [c["ip"] for c in candidates if c["region"] == "primary"] or [c["ip"] for c in candidates]else: result = [c["ip"] for c in candidates]
Route by customer ID in the qname — cust-<n>.svc.example.com → pin even/odd customers to different regions:
label = qname.split(".")[0] # e.g. "cust-42"region = "eu" if int(label.split("-")[1]) % 2 == 0 else "us"result = [c["ip"] for c in candidates if c["region"] == region] or [c["ip"] for c in candidates]
Drop unhealthy, then prefer lowest RTT (narrowing before a later selecting step):
healthy = [c for c in candidates if c["health_score"] >= 0.8]healthy = sorted(healthy, key=lambda c: c["rtt_ms"])result = [c["ip"] for c in healthy]
Put a selecting step (e.g. weighted-round-robin) after a narrowing script, or let the script return a single IP to act as the terminal selector.
Common Patterns
Geo-first with weighted fallback — prefer regional endpoints, select by weight among survivors:
["geo-ip", "weighted-round-robin"]
ISP-aware routing — prefer same-region, then same-ASN, then highest-priority failover:
["geo-ip", "asn", "failover"]
Map-file with latency tiebreak — steer by CIDR rule, then pick the fastest survivor:
["map-file", "latency"]
Pure failover — no geographic step; always prefer highest-priority healthy endpoint:
["failover"]
Backward Compatibility
Existing services that use the algorithm field continue to work without change. Filter chains are opt-in: if filterChain is absent or empty, the single algorithm value is used. The two modes are mutually exclusive at query time — filterChain takes precedence when set.
Interaction with Other Features
Composite health policy (
minHealthy): runs before filter chain dispatch. If the pool is below threshold, no filter chain step executes — the query returns no answer.Health scoring: candidates are pre-sorted by health score before the first filter runs. Unhealthy members (score = 0) are excluded from the initial candidate set.
EDNS0 Client Subnet: the
geo-ipfilter uses the client IP extracted from the ECS option when present, falling back to the resolver's source IP.Multi-tenancy:
filterChainis stored per-service and scoped to the owning tenant. It is included in NATS state-sync snapshots and replicated to peer nodes.DNS cache: the domain→service cache stores the full service record including
filterChain. Cache is invalidated on every service update.
Limitations
A chain of only narrowing filters (no terminal selector) returns
candidates[0]— typically the highest-health-score endpoint. Add a terminal filter to control selection explicitly.The
geo-ipfilter's pass-through applies per-query: if a client's region has no matching members, all members are eligible for the next step. This is intentional — it prevents blackholing traffic when regional capacity is unavailable.Filter names must exactly match the values in the table above. Unknown filter names are rejected by the API with a validation error.
The
round-robinfilter's counter is per-service and is reset when the service is restarted or the member set changes.