Skip to content

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 backend metric label follows the name, so moving a resolver from 10.0.0.1:53 to 10.0.0.5:53 does not rename or split its time series.
  • Patch by name — control-plane overlays can target a backend by (pool, name) instead of repeating the full address.
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.