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