Pools and backends
This page explains how Conduit groups upstream DNS servers into pools, selects a backend within a pool, and forwards queries to it.
Overview
Pools and backends are the organizational pattern Conduit uses to route forwarded DNS queries.
A backend is a configured upstream destination Conduit forwards DNS queries to. Each backend carries settings that control how Conduit reaches and uses that destination (for example, address, port, an optional load-balancing weight, and an optional stable name).
A pool is a named group of backends. Rules and scripts select a pool by name; Conduit then picks one backend inside that pool (see Backend weights) and forwards the query.
On the dataplane, Conduit selects the pool and backend during the Route phase of each transaction, after request rules run. For the full query pipeline, see Architecture and packet path. If nothing sets a pool, Conduit uses the pool named default, or the first pool in configuration. If the selected pool is missing or has no backends, Conduit responds with SERVFAIL. A retry may target a different pool (Retries and transactions).
Configuration
Pools are declared under the top-level pools: key in the config file. Each pool has a unique name and an unordered list of backends. Duplicate pool names or a pool with an empty backends list are rejected when the config is loaded or validated. A runnable installation needs at least one pool with at least one backend.
Minimal example (single pool, single backend):
pools:
- name: default
backends:
- address: "127.0.0.1:5300"
weight: 100
| Field | Meaning |
|---|---|
name |
Pool identifier used by rules (set_pool, set_retry_pool), Rhai, and as the pool label on metrics. |
backends |
One or more upstream destinations in this pool. |
address |
Upstream resolver as ip:port (IPv6 addresses use bracket notation, for example [2001:db8::1]:53). |
weight |
Optional load-balancing weight; see Backend weights. |
name |
Optional stable identity for the backend — sets the backend metrics label and lets control-plane patches target it by (pool, name); see Backend names. |
sources_v4 |
Optional list of local IPv4 addresses for upstream egress to this pool’s backends; see Dual-stack forwarding. |
sources_v6 |
Optional list of local IPv6 addresses for upstream egress to this pool’s backends. |
For every pool and backend field, defaults, and validation rules, see Reference: pools.
Other top-level blocks (listeners, forward, rules, and so on) are required for a runnable config but are documented on their own pages. A complete minimal file appears in Minimal configuration.
Backend weights
When a pool contains more than one backend, Conduit distributes queries among them using each backend’s weight. Weights are positive integers; weight is optional — if omitted, the effective weight is 100.
Example — two backends with a 70/30 split:
pools:
- name: default
backends:
- address: "10.0.0.1:53"
weight: 70
- address: "10.0.0.2:53"
weight: 30
Over many queries, traffic approximates the configured weight ratio. On the first forward attempt for a query, Conduit uses this sticky weighted pick. On retries within the same pool, Conduit selects among backends not already used for that pool on that transaction — see Retries and transactions.
When pool health is enabled, Route selects only among backends whose applied health is up, using effective weight (configured weight × optional latency factor) instead of configured weight alone. Backends marked down receive no new queries until probes or operator controls mark them up again. See Backend health.
Pool weights can be changed at runtime through the control plane (for example via ApplyConfig); see Control plane workflows.
Backend health
Optional per-pool active health probing and a passive fast-trip on live forwards detect upstream failure and recovery. Health is disabled by default — without health.enabled: true, selection stays weight-based and failures are handled reactively through retries and forward timeouts.
| Concern | Summary |
|---|---|
| Eligibility | Only backends with applied health up are candidates at Route. |
| Effective weight | With latency_weighting: true, share scales by probe latency EWMA among eligible backends (floor 0.25 — latency never zeroes a backend). |
| Fail-open floor | min_eligible — when too few backends are up, Route treats all as eligible rather than failing the pool. Single-backend pools always fail open. |
| Drain / maintenance | conduitctl health set down freezes and marks applied down while probes keep updating observed. |
| Scripts | Read health at hook entry via runtime.routing() — Runtime API. |
Full behavior, operator controls, and scope precedence: Backend health. Config fields: Reference: health.
Backend names
By default a backend is identified by its address — that string is the backend label on metrics and the target for control-plane patches. Setting an optional name gives the backend a stable identity that does not change when you renumber the upstream.
Naming is useful when you want to:
- Keep dashboards stable — the
backendmetric label follows thename, so moving a resolver from10.0.0.1:53to10.0.0.5:53does not rename or split its time series. - Patch by name — control-plane overlays can target a backend by
(pool, name)instead of repeating the fulladdress.
pools:
- name: default
backends:
- address: "10.0.0.1:53"
name: resolver-a
weight: 70
- address: "10.0.0.2:53"
name: resolver-b
weight: 30
With names set, metrics for these upstreams carry backend="resolver-a" and backend="resolver-b" instead of the raw addresses. Backend names must be unique within their pool; the same name may be reused in a different pool. A backend without a name keeps using its address as the label.
For field types, defaults, and validation messages, see Reference: pools — Backend object.
Multiple pools
Define more than one pool when different queries should use different upstream groups — for example public recursive resolvers for the internet and a separate resolver for internal zones (split horizon).
Only queries that match a rule need an explicit set_pool; everything else uses the pool named default, or the first pool in the file if there is no default pool (see Overview).
pools:
- name: default
backends:
- address: "100.100.100.100:53"
weight: 100
- name: internal
backends:
- address: "10.0.1.53:53"
weight: 100
rules:
match_mode: first_match
rules:
- name: internal-zones
hook: request
selectors:
- type: qname_suffix
value: ".corp.example."
actions:
- type: set_pool
value: internal
Queries for names ending in .corp.example. use the internal pool; all other queries use default without an extra catch-all rule.
The pool name in set_pool, set_retry_pool (or in Rhai’s set_pool(...) / set_retry_pool(...)) must match a name under pools:. Additional selectors — query name, query type, response code, tags, and others — are covered on Rules and actions. Retries can send a later attempt to a different pool when an upstream fails.
Related topics
- Backend health — probing, eligibility, drain, and metrics
- Glossary — pool, backend, transaction, retry, freeze, drain
- Rules and actions — how request and response rules select pools
- Retries and transactions — failing over to another pool or backend
- Minimal configuration — smallest runnable config including
pools: - Architecture and packet path — pipeline phases and the Route step
- Reference: pools — config schema (field reference)