Single YAML file, passed as CLI argument or set via the
PRAXIS_CONFIG environment variable. See
examples/configs/ for working examples.
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 development.md.admin.address binds a separate HTTP listener that serves
/ready and /healthy. /healthy returns 200 OK with
{"status":"ok"} once the server is accepting
connections (liveness). /ready returns per-cluster
health status with healthy/unhealthy/total counts when
active health checks are configured; it returns 503
when any cluster has zero healthy endpoints. Without
health checks, /ready returns {"status":"ok"}. Any
other path returns 404. Useful for orchestrator health
checks 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.
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.
| Filter | Category | Protocol |
|---|---|---|
router |
Traffic Management | HTTP |
load_balancer |
Traffic Management | HTTP |
timeout |
Traffic Management | HTTP |
static_response |
Traffic Management | HTTP |
rate_limit |
Traffic Management | HTTP |
headers |
Transformation | HTTP |
request_id |
Observability | HTTP |
access_log |
Observability | HTTP |
tcp_access_log |
Observability | TCP |
forwarded_headers |
Security | HTTP |
guardrails |
Security | HTTP |
ip_acl |
Security | HTTP |
json_body_field |
Payload Processing | HTTP |
compression |
Payload Processing | HTTP |
cors |
Security | HTTP |
redirect |
Traffic Management | HTTP |
path_rewrite |
Transformation | HTTP |
url_rewrite |
Transformation | HTTP |
model_to_header |
AI / Inference | HTTP (requires ai-inference feature) |
Routes requests to clusters by path prefix. Longest prefix
wins. Optional host restricts matching to a specific
Host header. Optional headers restricts matching to
requests with all specified header values present (AND
semantics, case-sensitive). Routes without host match
any host.
Example configs: path-based-routing.yaml, hosts.yaml, canary-routing.yaml.
Strategies:
round_robin(default): cycles through endpointsleast_connections: picks endpoint with fewest active requestsconsistent_hash: hashes a request header (or URI path as fallback) to pin requests to stable endpoints
Example configs: weighted-load-balancing.yaml, least-connections.yaml, session-affinity.yaml.
Cluster-level options: connection_timeout_ms,
total_connection_timeout_ms, idle_timeout_ms,
read_timeout_ms, write_timeout_ms, tls.
total_connection_timeout_ms sets the combined budget for
TCP connect and TLS handshake. When used alongside
connection_timeout_ms, the difference is effectively the
TLS handshake budget. It must be >= connection_timeout_ms.
Cluster tls enables TLS to the upstream. See
tls.md for full details on upstream TLS, mTLS,
CA trust, and certificate verification.
Clusters support active health checks via the
health_check field. Endpoints that fail consecutive
probes are removed from load balancer rotation until they
recover. See health-checks.yaml.
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | required | "http" or "tcp" ("grpc" parses but is not yet supported) |
path |
string | "/" |
HTTP path to probe (HTTP only) |
expected_status |
integer | 200 | Expected HTTP status code |
interval_ms |
integer | 5000 | Probe interval in ms |
timeout_ms |
integer | 2000 | Per-probe timeout in ms |
healthy_threshold |
integer | 2 | Consecutive successes to mark healthy |
unhealthy_threshold |
integer | 3 | Consecutive failures to mark unhealthy |
TCP health checks only verify a TCP connection can be
established; path and expected_status are ignored.
When active health checks are configured, the admin
/ready endpoint reports per-cluster health counts.
By default, health check endpoints that resolve to loopback or cloud metadata addresses are rejected (SSRF protection).
Add headers to requests; add, set, or remove headers on responses:
- filter: headers
request_add:
- name: "X-Forwarded-Proto"
value: "https"
response_add:
- name: "X-Served-By"
value: "praxis"
response_set:
- name: "Server"
value: "praxis"
response_remove:
- "X-Powered-By"add appends (preserves existing), set replaces,
remove deletes. Request headers support add only.
Response headers support all three operations.
Returns 504 if upstream response exceeds configured duration:
- filter: timeout
timeout_ms: 5000Propagates an existing request ID header or generates a new one:
- filter: request_id
header_name: "X-Request-Id" # optional, this is the defaultStructured JSON logging of method, path, status, and timing:
- filter: access_logOptional sampling to reduce log volume:
- filter: access_log
sample_rate: 0.1 # log ~10% of requestsInjects X-Forwarded-For, X-Forwarded-Proto, and
X-Forwarded-Host into upstream requests:
- filter: forwarded_headers
trusted_proxies:
- "10.0.0.0/8"
- "172.16.0.0/12"When the client IP is from a trusted proxy, existing
X-Forwarded-For values are preserved. Otherwise, the
header is overwritten to prevent spoofing.
Allow or deny requests by source IP/CIDR:
- filter: ip_acl
allow:
- "10.0.0.0/8"
deny:
- "0.0.0.0/0"When allow is set, only matching IPs are permitted.
allow takes precedence over deny. Denied requests
receive a 403 Forbidden response.
Structured JSON logging of TCP connections. Works on both TCP and HTTP listeners:
- filter: tcp_access_logExtracts a top-level field from a JSON request body and promotes its value to a request header. Uses StreamBuffer mode to inspect the body before upstream selection, enabling body-based routing.
- filter: json_body_field
field: model
header: X-Modelfield is the JSON key to extract. header is the
request header name to promote the value into. If the
field is missing or the body is not valid JSON, the
filter passes through without modification.
Returns a fixed response without contacting any upstream. Useful for health checks, status endpoints, or stub routes:
- filter: static_response
status: 200
headers:
- name: Content-Type
value: application/json
body: '{"status": "ok", "server": "praxis"}'status is required. headers and body are optional.
Combine with conditions to serve static responses on
specific paths.
Token bucket rate limiter. Supports per_ip (one bucket
per source IP) and global (one shared bucket) modes.
Rejects excess traffic with 429 and Retry-After header.
Injects X-RateLimit-Limit, X-RateLimit-Remaining, and
X-RateLimit-Reset headers into both rejections and
successful responses.
- filter: rate_limit
mode: per_ip # "per_ip" or "global"
rate: 100 # tokens replenished per second
burst: 200 # maximum bucket capacity| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | yes | "per_ip" or "global" |
rate |
float | yes | Tokens per second (must be > 0) |
burst |
integer | yes | Max bucket capacity (must be >= rate) |
Rejects requests matching string or regex rules against headers and/or body content. Rejected requests receive 401 Unauthorized.
- filter: guardrails
rules:
- target: header
name: "User-Agent"
pattern: "bad-bot.*"
- target: body
contains: "DROP TABLE"
- target: body
pattern: "^\\{.*\\}$"
negate: trueEach rule has:
| Field | Type | Required | Description |
|---|---|---|---|
target |
string | yes | "header" or "body" |
name |
string | header only | Header name to inspect |
contains |
string | one of | Literal substring match |
pattern |
string | one of | Regex pattern match |
negate |
bool | no | Invert match (default: false) |
Each rule must have either contains or pattern, not
both. Body rules use Buffer mode (up to 1 MiB by default)
to inspect the full request body.
Spec-compliant CORS filter with preflight handling, origin validation, and credential support. See cors.yaml.
| Field | Type | Default | Description |
|---|---|---|---|
allow_origins |
list | required | Origins to allow; ["*"] for any |
allow_methods |
list | GET, HEAD, POST | Allowed HTTP methods |
allow_headers |
list | none | Allowed request headers |
expose_headers |
list | none | Response headers exposed to client |
allow_credentials |
bool | false | Include credentials header |
max_age |
integer | 86400 | Preflight cache duration (seconds) |
allow_private_network |
bool | false | Private Network Access support |
disallowed_origin_mode |
string | "omit" | "omit" or "reject" for non-matching origins |
allow_null_origin |
bool | false | Allow Origin: null |
Wildcard subdomain patterns (e.g. https://*.example.com)
are supported. allow_credentials: true is incompatible
with wildcard origins, methods, or headers per the Fetch
spec.
Returns a 3xx redirect without contacting any upstream:
- filter: redirect
status: 301
location: "https://example.com${path}"| Field | Type | Default | Description |
|---|---|---|---|
status |
integer | 301 | Redirect status (301, 302, 307, or 308) |
location |
string | required | URL template; ${path} and ${query} are substituted |
Rewrites the request path before forwarding to upstream.
Exactly one of strip_prefix, add_prefix, or replace
per filter instance. Query strings are preserved. See
path-rewriting.yaml.
| Field | Type | Description |
|---|---|---|
strip_prefix |
string | Remove this prefix from the path |
add_prefix |
string | Prepend this prefix to the path |
replace.pattern |
string | Regex pattern to match |
replace.replacement |
string | Replacement string ($1, $name captures) |
Regex-based path transformation and query string
manipulation. Operations applied in order:
regex_replace, strip_query_params,
add_query_params. See url-rewriting.yaml.
Gzip, brotli, and zstd response compression. All three enabled by default. See compression.yaml.
| Field | Type | Default | Description |
|---|---|---|---|
level |
integer | 6 | Default compression level (1-12) |
min_size_bytes |
integer | 256 | Skip responses smaller than this |
gzip |
object | enabled | Per-algorithm enabled and level |
brotli |
object | enabled | Per-algorithm enabled and level |
zstd |
object | enabled | Per-algorithm enabled and level |
content_types |
list | see above | MIME type prefixes that qualify |
At least one algorithm must be enabled.
when/unless gates on any filter chain entry. Request
predicates: path (exact match), path_prefix,
methods, headers. All fields within a condition are
ANDed. Use response_conditions with status or
headers predicates to gate response hooks. See
conditional-filters.yaml.
Request conditions gate both request and body hooks.
Response conditions gate only on_response and response
body hooks. A filter can have both conditions and
response_conditions.
Global hard ceilings on request and response payload
size. These apply across all body modes (Stream, Buffer,
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 MB
max_response_bytes: 5242880 # 5 MBBoth default to unlimited when omitted.
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.
runtime:
threads: 4
work_stealing: true
global_queue_interval: 61
upstream_keepalive_pool_size: 64upstream_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.
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 |
Praxis validates configuration at startup and fails closed. Ambiguous or risky settings are errors, not warnings. Insecure overrides (see development.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 development.md.