Single YAML file, passed as CLI argument or set via the
PRAXIS_CONFIG environment variable. See
examples/configs/ for working examples.
For individual filter configurations, see the Filter Reference.
listeners: # Required. Named listeners to bind.
filter_chains: # Named, reusable filter chains.
clusters: # Optional. Standalone cluster defs (health checks).
admin: # Optional. Admin health endpoint.
body_limits: # Optional. Global body size ceilings.
metrics: # Optional. Prometheus metric collection toggles.
runtime: # Optional. Thread pool and logging tuning.
shutdown_timeout_secs: # Optional. Graceful drain time (default: 30).
insecure_options: # Optional. Dev/test overrides. See developing/getting-started.md.Use --validate (or -t) to check configuration
without starting the server. The flag loads the config
through the same parsing and validation path used
during startup, including filter pipeline construction
and ordering checks.
praxis --validate --config praxis.yaml
praxis -t -c praxis.yamlExits 0 on success (no output). Exits non-zero and
prints an error to stderr on failure. Does not bind
listener ports or enter the server runtime.
Use --dump (or -T) to validate and dump the
effective parsed configuration as YAML to stdout. The
output includes the effective parsed config (with
defaults applied) plus resolved top-level listener
chains.
praxis --dump --config praxis.yaml
praxis -T -c praxis.yamlExits 0 on valid config, writing YAML to stdout.
Exits non-zero and writes errors to stderr on failure.
Does not start the proxy or bind listeners. --dump
and --validate are mutually exclusive.
Praxis watches the config file for changes and automatically reloads filter pipelines without restart or disruption. When the file is modified, the server validates the new config, rebuilds pipelines, and swaps them atomically. In-flight requests complete on the old pipeline; new requests pick up the new config.
If the new config is invalid (bad YAML, unknown filter, validation failure), the server logs the error and continues serving with the old config.
Dynamically reloadable:
- Filter pipeline configuration
- Metrics collection settings (
metrics.filter_duration) - Router routes and path mappings
- Load balancer endpoints and weights
- Rate limit and circuit breaker settings
- Health check configuration
Requires restart (logged as warning):
- Listener add, remove, or address rebind
- Protocol changes (HTTP to TCP)
- Compression module addition
- TLS enable/disable
Stateful filters (rate limiter, circuit breaker) reset their state on reload. Operators should expect a brief burst window for rate limiters and a closed circuit for circuit breakers immediately after reload.
See hot-reload.yaml for an example.
admin.address binds a separate HTTP listener that serves
/healthy, /ready, and /metrics.
/healthyreturns200 OKwith{"status":"ok"}once the server is accepting connections (liveness)./readyreturns per-cluster health status with healthy/unhealthy/total counts when active health checks are configured; it returns503 SERVICE UNAVAILABLEwhen any cluster has zero healthy endpoints. Without health checks,/readyreturns{"status":"ok"}./metricsreturns Prometheus text exposition format with HTTP request metrics (praxis_http_requests_total,praxis_http_request_duration_seconds). Whenmetrics.filter_durationis enabled, also exposes per-filter hook duration histograms (praxis_filter_duration_seconds, labels:filter,phase,stream).
Any other path returns 404 NOT FOUND. Useful for orchestrator
health checks and monitoring without exposing them on
the main listeners.
admin:
address: "127.0.0.1:9901"When admin.verbose: true, the /ready response
includes per-cluster detail (cluster names, health
counts). Default is false to avoid leaking internal
topology.
admin:
address: "127.0.0.1:9901"
verbose: trueBy default, the admin endpoint must bind to a loopback
address (127.0.0.1 or [::1]). Binding to any
non-loopback address (including 0.0.0.0 / [::] or a
LAN IP) is a validation error unless
insecure_options.allow_public_admin: true is set.
listeners:
- name: web
address: "0.0.0.0:8080"
filter_chains:
- observability
- routing
filter_chains:
- name: observability
filters:
- filter: request_id
- filter: access_log
- name: routing
filters:
- filter: router
routes:
- path_prefix: "/api/"
cluster: api
- path_prefix: "/"
cluster: web
- filter: load_balancer
clusters:
- name: api
endpoints: ["127.0.0.1:4000"]
- name: web
endpoints: # multi-line form
- "127.0.0.1:3000" # (equivalent to inline
- "127.0.0.1:3001" # array above)Each listener has a required name, an address, optional
tls, optional protocol (defaults to http), and an
optional list of filter_chains to apply. When
filter_chains is omitted it defaults to empty (no filters
applied).
listeners:
- name: public
address: "0.0.0.0:80"
filter_chains: [main]
- name: secure
address: "0.0.0.0:443"
filter_chains: [main]
tls:
certificates:
- cert_path: /etc/praxis/tls/cert.pem
key_path: /etc/praxis/tls/key.pemThe name field uniquely identifies the listener and is
used to resolve its pipeline at startup.
Binding to 0.0.0.0 or [::] exposes the listener
on all network interfaces. For local development,
prefer 127.0.0.1. In production, bind to specific
internal IPs and use firewall rules to restrict
access. The default configuration binds to
127.0.0.1:8080 as a security precaution.
TCP listeners set protocol: tcp and require either a
static upstream address or a cluster name for load-
balanced routing. Filter chains are optional. The two
fields are mutually exclusive.
listeners:
- name: postgres
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"Optional tcp_session_timeout_ms sets a hard deadline
for TCP connections. Active connections are terminated
after this duration regardless of activity:
listeners:
- name: postgres
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"
tcp_session_timeout_ms: 300000 # 5 minutesOptional tcp_max_duration_secs caps the total session
duration regardless of activity:
listeners:
- name: postgres
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"
tcp_max_duration_secs: 3600 # 1 hourOptional downstream_read_timeout_ms sets how long the
proxy waits for data from downstream clients during body
reads. Mitigates slow-body attacks on HTTP listeners.
listeners:
- name: web
address: "0.0.0.0:8080"
downstream_read_timeout_ms: 10000 # 10 seconds
filter_chains: [main]Pingora applies its own 60s default for initial request header reads on fresh connections. This setting controls body read timeouts within an active request.
Optional max_connections caps concurrent connections
per listener. HTTP listeners reject excess requests
with 503 Service Unavailable and a Retry-After: 1
header. TCP listeners close the socket immediately.
listeners:
- name: public
address: "0.0.0.0:8080"
max_connections: 10000
filter_chains: [main]The limit is enforced via a per-listener semaphore. Permits are held for the request lifetime (HTTP) or connection lifetime (TCP) and released automatically on completion, error, or timeout. Each listener has an independent limit.
See max-connections.yaml for an example.
HTTP and TCP listeners can run on a single server instance. Each listener gets its own filter chains appropriate to its protocol.
listeners:
- name: web
address: "0.0.0.0:8080"
filter_chains: [routing]
- name: db
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"See tls.md for TLS details.
Named filter chains are defined at the top level. Each chain
has a name and an ordered list of filters. Listeners
reference chains by name via filter_chains:.
filter_chains:
- name: security
filters:
- filter: headers
response_set:
- name: "X-Content-Type-Options"
value: "nosniff"
- name: observability
filters:
- filter: request_id
- filter: access_log
- name: routing
filters:
- filter: router
routes:
- path_prefix: "/"
cluster: backend
- filter: load_balancer
clusters:
- name: backend
endpoints: ["10.0.0.1:8080"]A listener can reference multiple chains. The filters from each chain are concatenated in order to form the listener's complete pipeline. This enables reuse without duplication.
listeners:
- name: public
address: "0.0.0.0:8080"
filter_chains:
- security
- observability
- routing
- name: internal
address: "0.0.0.0:9090"
filter_chains:
- observability
- routingThe public listener runs security + observability + routing. The internal listener skips security but shares the same observability and routing chains.
Filters are protocol-aware. HTTP filters (e.g. router,
load_balancer) only work on HTTP listeners. TCP filters
(e.g. tcp_access_log) work on both HTTP and TCP listeners.
An HTTP listener's protocol stack includes TCP, so it
supports TCP-level filters too.
Global hard ceilings on request and response payload
size. These apply across all body modes (Stream,
StreamBuffer). When a filter also declares a per-filter
max_bytes, the smaller of the two limits is enforced.
Requests exceeding the limit receive 413 (Payload Too
Large).
body_limits:
max_request_bytes: 10485760 # 10 MiB
max_response_bytes: 5242880 # 5 MiBBoth default to 10 MiB (10,485,760 bytes) when
omitted. Setting either to null removes the ceiling
but requires insecure_options.allow_unbounded_body: true; without that flag, startup fails with a
validation error.
Optional Prometheus metric collection toggles. HTTP
request metrics on /metrics are always recorded when
the admin endpoint is enabled. Per-filter hook duration
histograms are opt-in.
metrics:
filter_duration: truefilter_duration defaults to false. When enabled,
records wall-clock duration for each HTTP filter hook
(request/response headers and body). Histogram labels:
filter (filter name), phase (request or
response), stream (headers or body).
Requires admin.address to scrape via /metrics.
Enabling filter_duration without admin logs a startup
warning; metrics are recorded but not exposed.
Praxis inherits header and request limits from Pingora's HTTP/1.x parser. These are compile-time constants in Pingora and are not currently configurable in Praxis.
| Limit | Value | Notes |
|---|---|---|
| Max total header size | 1,048,575 B (~1 MiB) | Includes request line |
| Max number of headers | 256 | HTTP/1.x only |
| Request-URI max size | shared with header limit | No separate cap |
| Header read timeout | 60 s | Pingora default |
| Body buffer chunk | 65,536 B (64 KiB) | Per-read buffer |
HTTP/2 header limits are governed by the h2 crate's
HPACK and frame-level settings (typically 16 KiB for
HEADERS frames by default, negotiated via SETTINGS).
Requests that exceed header size or count limits receive a 400 Bad Request from Pingora before reaching the filter pipeline.
Worker thread pool and scheduling configuration.
runtime:
threads: 8 # 0 = auto-detect (default)
work_stealing: true # default: true-
threads: number of worker threads per service. When set to 0 (the default), the thread count is auto-detected from available CPUs. -
work_stealing: allow work-stealing between worker threads of the same service. Enabled by default. -
global_queue_interval: fixed global queue interval for the tokio scheduler.Option<u32>, defaults toSome(61). Set tonullto use tokio's default. -
upstream_keepalive_pool_size: maximum number of idle upstream connections kept per thread.Option<usize>, defaults toSome(64). Set tonullto disable keepalive pooling. -
max_connections: process-wide maximum concurrent connections across all listeners. When set, new connections beyond this limit are rejected.Option<u32>, defaults toNone(disabled). Distinct from per-listenermax_connections. -
max_memory_bytes: process-wide RSS memory limit for load shedding. When set, the proxy monitors resident memory and rejects new requests with503 Service Unavailablewhen usage exceeds the threshold.Option<usize>, defaults toNone(disabled). -
subrequest_circuit_breaker: per-peer circuit breaker for the shared sub-request connector used byiterative_request_router. When configured, the connector tracks consecutive transport failures per upstreamSocketAddrand rejects sub-requests while a peer's circuit is open. Fields:consecutive_failures: failure threshold before the circuit opens (required, must be > 0).recovery_window_secs: seconds the circuit stays open before allowing a probe (required, must be > 0).half_open_timeout_secs: seconds a half-open probe may remain in-flight before the circuit resets to open (default 30).
The circuit breaker state is preserved across config reloads. Use the
transport_error: circuit_opentransition initerative_request_routerstep rules to match circuit-open rejections.
runtime:
threads: 4
work_stealing: true
global_queue_interval: 61
upstream_keepalive_pool_size: 64
max_connections: 10000 # process-wide limit
max_memory_bytes: 1073741824 # 1 GiB
subrequest_circuit_breaker:
consecutive_failures: 5
recovery_window_secs: 30upstream_ca_file sets a PEM CA file used as the root
certificate store for all upstream TLS connections.
Per-cluster tls.ca overrides this for individual
clusters.
runtime:
upstream_ca_file: /etc/praxis/tls/internal-ca.pemThis replaces the system trust store (not additive). See tls.md for details on CA trust precedence and combined bundles.
Set PRAXIS_LOG_FORMAT=json to emit structured JSON log
output instead of the default human-readable format.
Per-module log level overrides can be configured under
runtime.log_overrides:
runtime:
log_overrides:
praxis_filter::pipeline: trace
praxis_protocol: debugThis is useful for debugging a specific subsystem without flooding output from every module.
In-memory key-value stores for runtime-updatable
mappings. Stores are created dynamically by filters
at runtime via KvStoreRegistry::get_or_create and
managed through the admin API. No YAML configuration
is required.
Filters access stores by name through
HttpFilterContext and TcpFilterContext.
Stores support four match types for key lookup:
| Type | Behavior |
|---|---|
exact |
Key must equal the lookup key |
prefix |
Stored key starts with the pattern |
suffix |
Stored key ends with the pattern |
regex |
Stored key matches a regex pattern |
When admin.address is configured, CRUD endpoints
are available:
| Method | Path | Description |
|---|---|---|
GET |
/api/kv/{store} |
List all entries |
GET |
/api/kv/{store}/{key} |
Get a value |
PUT |
/api/kv/{store}/{key} |
Set a value (body) |
DELETE |
/api/kv/{store}/{key} |
Delete a key |
Writes are immediately visible to all filters on all threads. Unknown store names return 404.
Key-value stores are runtime caches, not durable storage. Data lives in memory and is lost on process exit.
The store is designed for operational overrides (routing tables, feature flags, config knobs) that can be reconstructed from an external source of truth. Do not use it as a primary data store.
The KvBackend trait allows alternative implementations
(e.g. Redis). The default InMemoryKvBackend uses
DashMap for lock-free reads. See the praxis_core::kv
module docs for the trait definition.
The shutdown_timeout_secs field controls how long the
server drains in-flight connections before forcing
shutdown:
shutdown_timeout_secs: 60 # default: 30When no configuration file is provided, Praxis starts with
a built-in default config that listens on 127.0.0.1:8080
and responds with {"status": "ok", "server": "praxis"}
on / (exact match) and 404 elsewhere. The default binds
to localhost only, preventing accidental exposure to
public networks during initial setup. This allows zero
config startup for testing. The source lives in
default.yaml. For a realistic starting point, see
basic-reverse-proxy.yaml.
Working examples live under examples/configs/, organized
by category:
| Directory | Contents |
|---|---|
branching |
Branch chains: conditional skip, terminal, reentrance, cross-chain |
traffic-management |
Router, load balancing, timeouts, redirects, rate limiting, static responses, P2C, canary, circuit breaker, health checks, gRPC detection |
payload-processing |
Compression, JSON field extraction, stream buffering, size limits |
security |
CORS, CSRF, IP ACL, guardrails, policy (feature-gated), forwarded headers, downstream read timeout |
observability |
Access logs, request IDs, TCP access logs |
transformation |
Headers, path rewrite, URL rewrite |
protocols |
TCP, TLS, mixed protocol configs |
pipeline |
Chain composition, conditions, failure mode, branch chains |
operations |
Hot reload, admin, multi-listener, max connections, production gateway |
Conditional sub-pipelines based on filter results: short-circuit responses, skip filters, retry loops, and cross-chain routing. See Branch Chains and Pipeline Concepts.
Per-cluster active HTTP/TCP probes and passive inline failure tracking remove unhealthy endpoints from rotation. See health-checks.yaml.
Filters declare failure_mode: open (continue on error)
or closed (reject the request). Security-critical
filters reject open mode by default. See
failure-mode.yaml.
Praxis validates configuration at startup and fails closed. Ambiguous or risky settings are errors, not warnings. Insecure overrides (see getting-started.md) require explicit opt-in and emit warnings at startup.
Key validations: listener name uniqueness, filter chain reference resolution, TLS path traversal rejection, admin endpoint binding restrictions, health check SSRF protection, upstream TLS SNI requirements, and payload size enforcement.
Praxis fails fast at startup for configuration problems. Common failure modes:
- Invalid YAML or missing required fields: the process exits with a descriptive error before any listener binds.
- Unknown filter chain reference: a listener references
a chain name not defined in
filter_chains:; caught at config validation. - TLS certificate load failure: the process exits if
a certificate's
cert_pathorkey_pathcannot be read or parsed. - Address bind failure: if the listen address is already in use or invalid, the server fails to start.
At runtime:
- Unreachable upstream: the request returns 502 (Bad Gateway). Connection timeouts are configurable per cluster.
- Filter error: an
Errfrom a filter results in a 500 response to the client. The error is logged. - Payload too large: exceeding
body_limits.max_request_bytesor a filter'smax_bytesreturns 413.
Some validations and features can be overridden for development
and testing purposes. See insecure_options in
getting-started.md.