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.
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
- 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).
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, binding admin to a public interface
(0.0.0.0 / [::]) is a validation error.
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 an upstream
address. Filter chains are optional for TCP listeners.
listeners:
- name: postgres
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"Optional tcp_idle_timeout_ms closes connections that have
been idle longer than the specified duration:
listeners:
- name: postgres
address: "0.0.0.0:5432"
protocol: tcp
upstream: "10.0.0.1:5432"
tcp_idle_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.
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: truethreads: 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_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).
runtime:
threads: 4
work_stealing: true
global_queue_interval: 61
upstream_keepalive_pool_size: 64
max_memory_bytes: 1073741824 # 1 GiBupstream_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 |
|---|---|
ai |
AI inference model-to-header routing |
traffic-management |
Router, load balancer, timeouts, static responses, redirects, rate limiting, health checks |
payload-processing |
Body processing: compression, field extraction, stream buffering, size limits |
security |
Forwarded headers, IP ACL, guardrails, CORS, downstream read timeout |
observability |
Access logs, request IDs |
transformation |
Header manipulation, path rewriting, URL rewriting |
protocols |
TCP, TLS, mixed protocol configs |
pipeline |
Filter chain composition and conditions |
operations |
Production gateway, multi-listener, admin |
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.