Readyset Docs

Router Configuration

Complete reference for querypilot.toml, the configuration file for the QueryPilot Router.

Overview

The Router (binary querypilot) is configured by a single TOML file, querypilot.toml. The Router locates it from, in order of precedence: the --config / -c flag, the QUERYPILOT_CONFIG environment variable, or querypilot.toml in the current working directory.

The schema is strict: unknown keys anywhere in the file are rejected at startup with a parse error, so typos fail loudly instead of being silently ignored. Duration values are humantime strings such as "30s", "10m", or "750ms". Run querypilot --check -c <path> to validate a file without serving traffic.

Client authentication comes from pool credentials: clients authenticate with the username and password of a pool that defines them. If no pool defines credentials and allow_anonymous is false, the Router refuses to start.

MySQL support is in preview. MySQL listeners, MySQL pools, and the MySQL-specific keys on this page are functional, but PostgreSQL is the supported GA v1 path.

[general]

Global proxy settings.

worker_threads

Number of Tokio worker threads. 0 means one thread per CPU core.

Type: integer Default: 0

pool_mode

When a backend connection is returned to the pool: at session end ("session"), at transaction end ("transaction"), or after each statement ("statement"). Individual pools can override this with mode.

Type: string, one of "session", "transaction", "statement" Default: "transaction"

shutdown_timeout

How long graceful shutdown waits for in-flight sessions before exiting.

Type: duration Default: "30s"

allow_anonymous

Permit the Router to start with no client authentication on a non-loopback listener. Without configured pool credentials the Router accepts any client, so this is off by default and the Router refuses to boot in that situation. Leave false in production.

Type: bool Default: false

[general]
pool_mode = "transaction"
shutdown_timeout = "30s"
worker_threads = 0

[[listeners]]

Client-facing listeners. At least one is required.

protocol

Wire protocol served on this listener.

Type: string, one of "postgres", "mysql" Default: required, no default

bind

Address and port to listen on. The port must not be 0, and no two listeners may share a bind address.

Type: socket address, for example "0.0.0.0:6432" Default: required, no default

tls_mode

TLS posture for this listener. Any value other than "disable" requires a [tls] section to be present. The value is parsed and validated but not enforced in GA v1; see the TLS section below.

Type: string, one of "disable", "prefer", "require" Default: "disable"

[[listeners]]
protocol = "postgres"
bind = "0.0.0.0:6432"

[[listeners]]
protocol = "mysql"
bind = "0.0.0.0:6433"

[pools.<name>]

Named backend pools. Routing must resolve to an existing pool, so you need at least one.

backends

Backend servers in this pool. Non-empty array of inline tables; see backend entries below.

Type: array of tables Default: required, no default

mode

Per-pool override of general.pool_mode. Unset means inherit.

Type: string, one of "session", "transaction", "statement" Default: unset

protocol

Force the backend wire protocol for this pool regardless of the client session's protocol. Useful for routing MySQL clients to a PostgreSQL-speaking backend such as Analytics. Incompatible with raw_passthrough.

Type: string, one of "postgres", "mysql" Default: unset (use the client session's protocol)

username

Backend login user. This also serves as the client-authentication credential: clients authenticate against the Router with pool credentials. Unset means pass through the client's credentials.

Type: string Default: unset

password

Backend login password.

Type: string Default: unset

database

Backend database override. Unset means use the client's database.

Type: string Default: unset

raw_passthrough

Forward prepared-statement result rows from the backend to the client without decoding. Enable only when the backend is vanilla PostgreSQL whose wire encoding matches the client exactly. Leave off for Analytics or any cross-protocol pool, where rows must be decoded and re-encoded. Rejected if a protocol override is set on the same pool.

Type: bool Default: false

min_connections

Connections kept open per backend. Must be less than or equal to max_connections.

Type: integer Default: 1

max_connections

Upper bound on pooled connections. At most 10000.

Type: integer Default: 10

acquire_timeout

How long a session waits for a free backend connection before failing.

Type: duration, non-zero Default: "30s"

idle_timeout

Idle backend connections above min_connections are closed after this.

Type: duration, non-zero Default: "600s"

connect_timeout

TCP and startup timeout when dialing a backend.

Type: duration, non-zero Default: "10s"

query_timeout

Per-query deadline for this pool. Unset means no deadline: a query only fails when the backend itself returns an error. Sessions may install a tighter per-query override (for example from an Guard add_timeout amendment).

Type: duration Default: unset

role

Role used by read/write splitting.

Type: string, one of "primary", "replica" Default: "primary"

Backend entries

Each element of backends is an inline table with these keys.

host

Backend hostname or IP. Required, non-empty.

Type: string Default: required, no default

port

Backend port. Must not be 0.

Type: integer Default: 5432

weight

Load-balancing weight. Parsed and validated, but not applied in GA v1: backend selection is round-robin.

Type: integer, non-zero Default: 1

max_replica_lag

Skip this backend when its measured replication lag exceeds the threshold. Unset disables the lag check.

Type: duration Default: unset

[pools.primary]
role = "primary"
min_connections = 2
max_connections = 50
backends = [
    { host = "primary.db.local", port = 5432 }
]

[pools.replica]
role = "replica"
max_connections = 25
backends = [
    { host = "replica1.db.local", port = 5432 },
    { host = "replica2.db.local", port = 5432, max_replica_lag = "10s" }
]

[routing]

How the Router picks a pool for each query. See the read/write split tutorial for a worked example.

read_write_split

Route reads and writes to different pools automatically. When true, read_pool and write_pool must exist in [pools]; when false, default_pool must exist.

Type: bool Default: false

read_pool

Pool for read queries when splitting is enabled.

Type: string Default: "replica"

write_pool

Pool for write queries when splitting is enabled.

Type: string Default: "primary"

default_pool

Pool used when splitting is off and no rule matches.

Type: string Default: "primary"

mysql_default_pool

Default pool for connections that arrived on a MySQL listener. When set, queries on a MySQL listener that match no content rules fall back to this pool instead of default_pool. This lets you route by listener identity without writing a content rule for every plain MySQL query. MySQL support is in preview.

Type: string Default: unset

[routing]
read_write_split = true
read_pool = "replica"
write_pool = "primary"
default_pool = "primary"

[[routing.rules]]

Regex-based routing rules. Each rule needs exactly one action: pool or reject.

name

Rule name for logs and metrics. Required, non-empty.

Type: string Default: required, no default

match_user

Regex matched against the session user. Must compile.

Type: string (regex) Default: unset

match_database

Regex matched against the session database. Must compile.

Type: string (regex) Default: unset

match_query

Regex matched against the query text. Must compile.

Type: string (regex) Default: unset

match_listener_protocol

Restrict the rule to connections that arrived on a listener speaking this protocol. Omit to apply the rule on all listeners.

Type: string, one of "postgres", "mysql" Default: unset

pool

Route matching queries to this pool. The pool must exist. Exactly one of pool or reject is required.

Type: string Default: unset

reject

Reject matching queries with this error message. Exactly one of pool or reject is required.

Type: string Default: unset

priority

Evaluation order; lower numbers run first.

Type: integer Default: 0

[[routing.rules]]
name = "admin_to_primary"
match_user = "^admin$"
pool = "primary"
priority = 0

[[routing.rules]]
name = "block_dangerous"
match_query = "(?i)DROP|TRUNCATE"
reject = "Dangerous query blocked by proxy"
priority = -1

[[routing.pattern_rules]]

Pattern rules match on normalized query shape rather than raw text. They are evaluated after translation rules and before regex rules.

type

How pattern is interpreted: "template" treats it as a normalized example query, "regex" as a regular expression over the query text, and "hash" as a query fingerprint hash written as a decimal 64-bit integer. Fingerprint hashes can be obtained from GET /api/routing/test on the admin API.

Type: string, one of "template", "regex", "hash" Default: required, no default

pattern

The pattern to match. For type = "regex", must compile.

Type: string Default: required, no default

Pattern rules also take name (required), pool or reject (exactly one required), and priority (default 0) with the same semantics as regex rules above.

[[routing.pattern_rules]]
name = "analytics_to_olap"
type = "regex"
pattern = "(?i)FROM analytics_"
pool = "analytics"
priority = 0

[[routing.pattern_rules]]
name = "user_lookup"
type = "template"
pattern = "SELECT * FROM users WHERE id = 123"
pool = "replica"
priority = 10

[routing.cache]

Routing-decision cache settings.

enabled

Cache routing decisions per normalized query.

Type: bool Default: true

max_size

Maximum cached entries.

Type: integer Default: 10000

normalization_strategy

How queries are normalized into cache keys: "string" is faster, "ast" is more accurate, "auto" chooses per query.

Type: string, one of "auto", "ast", "string" Default: "string"

[translation]

Translation rules rewrite a query to different SQL text and pin it to a pool, typically to redirect specific queries to an analytics backend with a different schema layout. Literal values are parameterized on both sides and substituted at query time. On config reload, all translations are cleared and re-registered from the file; translations registered at runtime via the admin API are ephemeral.

[[translation.rules]]

original_sql

Concrete example of the incoming query, with literal values. Required, non-empty.

Type: string Default: required, no default

translated_sql

Concrete rewritten query to execute instead. Required, non-empty.

Type: string Default: required, no default

target_pool

Pool the translated query runs on. Must exist in [pools].

Type: string Default: required, no default

[[translation.rules]]
original_sql = "SELECT name, email FROM customers WHERE customer_id = 123"
translated_sql = "SELECT name, email FROM analytics.customers WHERE customer_id = 123"
target_pool = "analytics"

[tls]

Required if any listener sets tls_mode to "prefer" or "require".

The [tls] schema parses and is validated at startup, but the Router does not terminate TLS in GA v1. Client connections are accepted in cleartext regardless of tls_mode, and backend connections are dialed without TLS. Terminate TLS in front of the Router with a load balancer or sidecar. See Security.

cert_file

Server certificate in PEM format. The file must exist at startup.

Type: string (path) Default: required if the section is present

key_file

Private key in PEM format. The file must exist at startup.

Type: string (path) Default: required if the section is present

ca_file

CA bundle for client-certificate verification. If set, the file must exist.

Type: string (path) Default: unset

[metrics]

enabled

Serve Prometheus metrics (the sqp_* series; see the metrics reference). Enabling metrics also starts the admin API.

Type: bool Default: false

bind

Metrics listen address. The default is loopback by design to keep the management surface off public networks.

Type: socket address Default: "127.0.0.1:9090"

[metrics]
enabled = true
bind = "127.0.0.1:9090"

The admin API address is derived from this section, not configured separately: it listens on the bind IP at the bind port plus one (9090 gives 9091). Binding metrics to 0.0.0.0 therefore also exposes the mutating admin surface on all interfaces. Set api_key on the plugin blocks where available, and firewall both ports. If the metrics port is 65535, startup fails with a port-overflow error.

[logging]

level

Log verbosity. This key accepts only the five listed levels. The --log-level flag and QUERYPILOT_LOG_LEVEL environment variable override it at launch and accept richer tracing filter directives; see environment variables.

Type: string, one of "trace", "debug", "info", "warn", "error" Default: "info"

format

Human-readable or structured JSON output.

Type: string, one of "pretty", "json" Default: "pretty"

[stats]

Per-query digest statistics, the data source for GET /api/stats/digests and Accelerator discovery.

max_entries

Maximum retained per-query statistics entries (distinct query fingerprints). New fingerprints arriving after the cap are dropped and counted into an overflow metric. Must be less than 65535 when [plugins.accelerator] is present and enabled.

Type: integer Default: 10000

[plugins.guard]

Configuration for Guard, the query-governance plugin. Guard has no standalone config file; this block is its entire operator surface. It additionally reads runtime artifacts pointed to by config: YAML policy files (policy_dir) and a YAML trusted-query registry (registry_file). See Guard and the policies tutorial.

enabled

Turn Guard on. When false, nothing is registered and the section is inert.

Type: bool Default: false

default_trust_level

Trust level assigned to sessions no trust rule matches.

Type: string, one of "untrusted", "verified", "established", "privileged" Default: unset (falls back to "verified")

policy_dir

Directory of YAML policy files. Paths containing .. are rejected.

Type: string (path) Default: unset (no policies loaded)

registry_file

Trusted-query registry file, YAML. Paths containing .. are rejected.

Type: string (path) Default: unset

notify_on_amend

Emit a client notice when Guard amends a query.

Type: bool Default: true

unanalyzable_action

What to do with a statement Guard cannot structurally analyze (parse failure, size or nesting cap, decision timeout) whose coarse type is not a plain read, for sessions below privileged. "log" allows the query but logs a warning and increments sqp_guard_unanalyzable_total; "block" is the strict posture; "allow" silences the signal.

Type: string, one of "allow", "log", "block" Default: "log"

enforcement_mode

Whether decisions are applied ("enforce"), surfaced as warnings without blocking ("warn"), evaluated but discarded ("shadow", useful for canarying a policy), or skipped entirely ("off").

Type: string, one of "off", "shadow", "warn", "enforce" Default: "enforce"

audit_sink

Where audit records go, as a tagged inline table. type = "stdout" writes one JSON line per record to stdout (suited to container deployments shipping logs via a sidecar). type = "file" requires a path and appends JSONL to it; the file is created with mode 0640 on Unix, and OS log rotation handles size. These are the only two supported sink types.

Type: inline table, { type = "stdout" } or { type = "file", path = "..." } Default: { type = "stdout" }

max_decision_latency_ms

Per-decision time budget in milliseconds. Beyond this, Guard fails open with Allow and increments sqp_guard_timeouts_total.

Type: integer Default: unset (falls back to 50)

max_registry_size

Maximum trusted-query registry entries.

Type: integer Default: unset (falls back to 100000)

audit_buffer_size

In-memory audit ring-buffer capacity, in records.

Type: integer Default: unset (falls back to 10000)

audit_channel_size

Audit channel depth between the producer and the drain task.

Type: integer Default: unset (falls back to 4096)

api_key

Key required by Guard's mutating REST endpoints (sent as the X-Guard-Api-Key header). Unset leaves them open, which is acceptable only for loopback development. See the admin API.

Type: string Default: unset

trust_rules

Per-session trust assignment rules, as [[plugins.guard.trust_rules]] tables. Rules are applied in declaration order; the last matching rule wins.

Type: array of tables Default: []

Each trust rule takes:

match

Inline table of conditions; all present fields must match. match.role is a glob against the session role (for example "service-*"), match.username is an exact username, match.origin is an exact client IP. An empty table matches every session.

Type: inline table Default: {}

set_level

Trust level to assign on match. Same values as default_trust_level. Required.

Type: string Default: required, no default

reason

Free-form note recorded in the audit log.

Type: string Default: unset

[plugins.guard]
enabled = true
policy_dir = "/etc/querypilot/guard-policies"
registry_file = "/var/lib/querypilot/guard-registry.yaml"
enforcement_mode = "enforce"
audit_sink = { type = "file", path = "/var/log/querypilot/guard-audit.jsonl" }
api_key = "<your-api-key>"

[[plugins.guard.trust_rules]]
match = { role = "service-*" }
set_level = "established"
reason = "internal service accounts"

[plugins.accelerator]

Configuration for the Accelerator, the caching plugin that discovers hot queries and manages Readyset caches. [plugins.accelerator] is the Accelerator's config block. The block is validated at boot only when enabled = true. See Accelerator and the caching tutorial.

enabled

Turn the Accelerator on. When false, nothing is registered and the plugin incurs no per-query or background cost.

Type: bool Default: false

interval_s

Seconds between Accelerator discovery cycles. Must be at least 1.

Type: integer Default: 10

dry_run

Log caching decisions without creating caches or mutating rules.

Type: bool Default: false

database

Wire protocol of the Readyset endpoints. MySQL support is in preview.

Type: string, one of "postgresql", "mysql" Default: "postgresql"

denylist_path

File listing queries never to cache.

Type: string (path) Default: unset (falls back to /etc/readyset_query_pilot_denylist)

readyset_servers

Readyset SQL endpoints the Accelerator connects to for support checks and CREATE CACHE, as an array of { host, port } tables. The plugin needs at least one to do useful work.

Type: array of { host, port } Default: []

readyset_user

User for connecting to Readyset.

Type: string Default: ""

readyset_password

Password for Readyset.

Type: string Default: ""

readyset_database

Database or schema on the Readyset side, when applicable.

Type: string Default: unset

min_execution

Minimum execution count before a query shape is a caching candidate.

Type: integer Default: 1

wait_s

Discovery warmup: a query shape must have been observable for at least this many seconds before it is a candidate.

Type: integer Default: 0

warmup_time_s

Rule warmup window in seconds. When greater than 0, a new rule is installed disabled and enabled after the window (mirror first, then serve). 0 enables rules immediately.

Type: integer Default: 0

api_key

Key required by the Accelerator's mutating REST endpoints (sent as the X-QueryPilot-Api-Key header). When unset and the admin API binds a non-loopback address, the mutating surface refuses to serve. See the admin API.

Type: string Default: unset

[plugins.accelerator.grants]

The operator's least-privilege contract for the plugin: what the Accelerator is allowed to touch.

pools

Pools the Accelerator may write routing rules for. Must list at least one pool when the plugin is enabled; an empty grant is rejected at boot.

Type: array of strings Default: []

max_owned_rules

Cap on routing rules the plugin may own at once. Must be at least 1 when set; 0 is rejected at boot because it would refuse every rule.

Type: integer Default: unset (unlimited)

[plugins.accelerator]
enabled = true
interval_s = 10
database = "postgresql"
readyset_servers = [{ host = "readyset.internal", port = 5433 }]
readyset_user = "rs_user"
readyset_password = "<your-password>"
warmup_time_s = 60
api_key = "<your-api-key>"

[plugins.accelerator.grants]
pools = ["readyset"]
max_owned_rules = 100