TOML, one file, validated at startup and on every reload.
Invalid configs exit non-zero at startup and are rejected (keeping the running config) on reload. Fields marked planned parse without error so configs stay forward-compatible, but torana logs a warning rather than silently ignoring them.
| Field | Status | Notes |
|---|---|---|
global.log_level | enforced | error · warn · info (default) · debug · trace |
global.log_format | enforced | json (default) or plain text |
global.metrics_addr | enforced | metrics + health listener, default 127.0.0.1:9090 |
global.workers | ignored | Tokio sizes its own thread pool |
listener.addr | enforced | socket address; binds fail fast at startup |
listener.protocol | enforced | http or https |
listener.tls_cert / tls_key | enforced | PEM paths; required when protocol = https unless acme is set |
listener.tls_client_ca | enforced | mTLS: CA PEM path; client certs become mandatory; not combinable with acme |
listener.acme.domains/contact_emails/cache_dir | enforced* | automatic HTTPS, RFC 8555, *needs --features acme |
listener.acme.directory_url/staging/ca_cert | enforced* | target a private ACME server or LE staging, *needs --features acme |
route.name | enforced | identifier used in logs and metrics |
route.host | enforced | exact hostname, or "*.example.com" for subdomains; unset matches any host |
route.path | enforced | prefix match on segment boundaries; unset matches any path |
route.upstream[].addr | enforced | http://host:port; https upstreams are rejected |
route.upstream[].weight | enforced | relative integer weight, default 100, max 10,000 |
route.retries | enforced | max attempts for GET/HEAD/OPTIONS with no body; default 2 with multiple upstreams |
route.health_check.* | enforced | path/interval/timeout; absent = always healthy |
route.plugin | enforced* | WASM request filter path, *needs --features plugins |
route.timeout.total | enforced | duration to upstream response headers, default 30s |
route.timeout.connect / first_byte | planned | only total is enforced |
route.when | planned | reserved for a future CEL-based matcher; use host/path today |
route.headers.request / .response | enforced | per-direction overrides; empty value removes the header |
route.mirror.addr / .rate | enforced | fire-and-forget; GET/HEAD/OPTIONS with no body only |
Duration strings accept ms, s, and m: "500ms", "30s", "5m". A bare number is rejected; units are mandatory.
Weights are relative, reduced by their greatest common divisor: 90 / 10 produces the same schedule as 9 / 1. An upstream with no weight gets 100. The scheduler interleaves upstreams rather than sending long runs to one host.
Routes are matched in config order: first match wins. Put specific host/path combinations first and a constraint-free route last as a catch-all. A request that matches no route gets 404.
[[route]] name = "api" host = "api.example.com" path = "/v1" upstream = [{ addr = "http://127.0.0.1:3000" }] retries = 2 [route.health_check] path = "/healthz" interval = "10s" timeout = "2s" [[route]] # catch-all, no host/path, put it last name = "default" upstream = [{ addr = "http://127.0.0.1:4000" }]
Only GET, HEAD, and OPTIONS requests with an empty body are retried, and only on a connect-level failure: a request that reached the upstream is never retried, so a non-idempotent request is never double-executed. Retried attempts skip upstreams already tried this request and prefer upstreams the health check considers up.
Next: mTLS, header rewriting & mirroring, sandboxed WASM plugins, or HTTP/2 & automatic HTTPS.