Praxis Core - Rust on Pingora

Everything Isa Filter

A proxy framework where routing, load balancing, and security are all the same primitive - composed into pipelines, swapped at runtime, without dropping a request.

scroll to begin ↓
Part I - Foundations
Chapter 01

One primitive, composed endlessly

Most proxies bolt features on as special cases. Praxis takes a different approach: every behavior is a filter, and built-in filters use the exact same traits as user-written ones.

Cluster routing, load balancing, rate limiting, body inspection, TLS policy - all filters. They implement HttpFilter or TcpFilter, get composed into pipelines, and execute in declared order. That single decision makes Praxis a framework for building proxies, not just a proxy you configure.

Each filter returns a FilterAction telling the pipeline what to do next: Continue to the next filter, Reject to stop with an error, or TerminalResponse to return a complete response while still running response-phase filters. No special cases.

Four design goals drive everything: fast, secure by default, composable, and adaptive. The filter model is how all four are reached at once.
Chapter 02

Standing on Pingora

Praxis does not write its own event loop, TLS stack, or HTTP parser. It builds on Pingora - a battle-tested Rust proxy framework handling trillions of requests per day in production.

Connection handling, HTTP/1 and HTTP/2 framing, connection pooling, request smuggling prevention, the async runtime - all inherited. Praxis spends its code budget on the layer above: the filter model, configuration, and composition engine.

fig.01 - what Praxis owns vs. what it inherits
Praxis logic sits above adapters; the hardened network core is Pingora's.

Pingora is quarantined behind adapters. Only the protocol and core crates import it directly. The filter model and everything you build on top never touch Pingora types. That keeps the dependency contained and the framework surface clean.

The payoff: Praxis gets memory-safe Rust performance and a proven concurrency core for free, and inherits upstream improvements instead of maintaining its own network stack.
Chapter 03

Built wide, not tall

Praxis is a Cargo workspace of focused crates, not one monolith. Each crate has a single job and a clear boundary. Capabilities sit side by side and depend on each other explicitly.

fig.02 - the framework crates
Five framework crates, a thin binary on top, test band to the side.

praxis-proxy-filter

The pipeline engine, HttpFilter and TcpFilter traits, condition evaluation, branch chains, load balancing strategies, and every built-in filter. The heart of the composition model.

praxis-proxy-core

Configuration types (YAML via serde), validation, error types, server factory, subrequest client, circuit breaker, and memory pressure monitoring.

praxis-proxy-protocol

Protocol adapters translating Pingora callbacks into pipeline invocations. The only crate (with core) that imports Pingora directly.

praxis-proxy-tls

TLS configuration, certificate loading, SNI-based cert selection, hot-reloadable cert resolver, mTLS client auth, and CRL support.

On top sits praxis-proxy - the proxy binary, kept thin because the real capability lives in the framework crates below. Alongside them, a full test band: conformance, integration, resilience, security, and schema - each its own workspace member.

The dependency direction is strict and one-way: binary depends on framework crates, they depend on adapters, adapters depend on Pingora. Nothing points back up.

Part II - How Requests Move
Part II - The Pipeline
Chapter 04

Down the pipeline, and back up

A request enters and passes through filters in declared order. Each can read, rewrite, or short-circuit. On the way back, response filters run in reverse order - so the filter that opened a concern is the one that closes it.

fig.03 - request down, response up
Watch a request flow through the pipeline and back.

Along the way, filters populate a shared HttpFilterContext: the router sets the target cluster, the load balancer picks the upstream. Filters communicate through durable metadata (key-value pairs persisting the entire request lifetime), transient filter results (for branch conditions), and header mutations.

Filters also declare how they access the body. Three modes exist: Stream (pass-through), StreamBuffer (buffer up to N bytes before deciding), and SizeLimit (enforce a maximum). Multiple filters requesting different modes produces the most capable mode - modes only ratchet upward, never down.

Chapter 05

Named chains, flat pipelines

Filters are grouped into named chains - a "security" chain, an "observability" chain, a "traffic" chain. Each listener references the chains it wants, and at startup they are concatenated into one flat pipeline.

fig.04 - two listeners composing shared chains
A public listener adds security; an internal one skips it. Same chains, different composition.

Pipelining

Startup-time: which chains a listener concatenates into its pipeline. Structural and validated before any request arrives.

Routing

Request-time: the router filter picks an upstream cluster by path, host, and headers. Dynamic and per-request.

Chain boundaries disappear at runtime - the pipeline is a flat Vec<PipelineFilter> with pre-computed body capabilities. Validation catches problems at startup: load balancers without routers, unconditional static responses, security filters bypassed by branch chains.

Chapter 06

Swap the pipeline mid-flight

Praxis replaces filter pipelines at runtime without restarting or dropping in-flight requests. Each listener holds an ArcSwap<FilterPipeline>. Every request loads a snapshot pinned for its lifetime; a reload atomically stores a new pipeline.

fig.05 - atomic pipeline swap
In-flight requests finish on the old pipeline; new ones use the new one.

A file watcher monitors the config directory with 500ms debounce and content hashing to skip unchanged files. If validation fails, nothing changes. The new config is fully parsed, all pipelines rebuilt, and only then swapped in atomically. Exponential backoff (1s base, 60s max) handles transient errors.

What reloads dynamically: filter chains, cluster definitions, body limits, KV stores. What requires a restart: listener addresses, protocol type, TLS toggles. The distinction is detected by diffing and logged, never half-applied.

Part III - Capabilities
Part III - The Toolbox
Chapter 07

26 HTTP filters, 3 TCP, 4 load balancers

Praxis ships with a broad set of built-in filters covering security, traffic management, transformation, and observability. All use the same traits a custom filter would.

fig.06 - built-in filter categories
Every built-in filter composes, reorders, and hot-reloads like any other.

Security

cors, csrf, ip_acl, credential_injection, forwarded_headers, guardrails, peer_identity_trust, policy

Traffic

router, load_balancer, rate_limit, circuit_breaker, timeout, redirect, static_response

Transform

headers, path_rewrite, url_rewrite, compression (gzip, brotli, zstd)

Observe & Payload

access_log, request_id, json_body_field, json_rpc, grpc_detection

Four load balancing strategies are built in: weighted round-robin, least connections (atomic in-flight counters), consistent hashing (stable endpoint by header or path), and power-of-two-choices (sample two, pick the less loaded). All are health-aware and skip endpoints marked unhealthy by active health checks.

TCP filters cover sni_router (route by TLS SNI), tcp_load_balancer, and tcp_access_log. An AnyFilter wrapper lets HTTP and TCP filters coexist in one pipeline. HTTP listeners accept both; TCP listeners accept only TCP filters. Mismatches are caught at startup.