Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/pingora/llms.txt
Use this file to discover all available pages before exploring further.
The filters in ProxyHttp are isolated methods — request_filter cannot call upstream_peer directly, and there is no implicit state threading between them. Pingora solves this with CTX: a per-request object of a type you define, passed by mutable reference to every phase of the request lifecycle. CTX lets you parse something once in an early phase and use the result in a later phase, without re-reading or re-computing it. It also provides a clean place to accumulate request-scoped metadata like retry counts, timing marks, or routing decisions.
Defining CTX
You define the CTX type as an associated type on your ProxyHttp implementation. Pingora calls new_ctx() at the start of every new request to create a fresh instance.
pub struct MyProxy;
pub struct MyCtx {
beta_user: bool,
}
#[async_trait]
impl ProxyHttp for MyProxy {
type CTX = MyCtx;
fn new_ctx(&self) -> Self::CTX {
MyCtx { beta_user: false }
}
// ... other phases
}
If your proxy has no state to share between phases, you can use the unit type:
impl ProxyHttp for MyProxy {
type CTX = ();
fn new_ctx(&self) -> Self::CTX {}
}
The CTX object is dropped at the end of each request. Any data stored in it is automatically released — you do not need to clear it manually.
Passing Data Between Phases
A common pattern is to parse or check something in request_filter — where you have access to the full request — and then use that result in upstream_peer to make a routing decision. The alternative (reading the header in upstream_peer) works too, but doing it early and storing the result in CTX keeps each phase focused on a single concern.
The following example detects beta users by the presence of a beta-flag header in request_filter, then uses that flag in upstream_peer to route them to a different upstream:
pub struct MyProxy;
pub struct MyCtx {
beta_user: bool,
}
fn check_beta_user(req: &pingora_http::RequestHeader) -> bool {
// some simple logic to check if user is beta
req.headers.get("beta-flag").is_some()
}
#[async_trait]
impl ProxyHttp for MyProxy {
type CTX = MyCtx;
fn new_ctx(&self) -> Self::CTX {
MyCtx { beta_user: false }
}
async fn request_filter(
&self,
session: &mut Session,
ctx: &mut Self::CTX,
) -> Result<bool> {
ctx.beta_user = check_beta_user(session.req_header());
Ok(false)
}
async fn upstream_peer(
&self,
_session: &mut Session,
ctx: &mut Self::CTX,
) -> Result<Box<HttpPeer>> {
let addr = if ctx.beta_user {
info!("I'm a beta user");
("1.0.0.1", 443)
} else {
("1.1.1.1", 443)
};
let peer = Box::new(HttpPeer::new(addr, true, "one.one.one.one".to_string()));
Ok(peer)
}
}
Sharing State Across Requests
CTX is per-request — it cannot share data between requests by itself. For cross-request state like counters, caches, or feature flags, use standard Rust concurrency primitives: Arc, Mutex, AtomicUsize, or static variables. Fields on the proxy struct itself are a natural home for this data, since the struct is shared across all requests.
The example below extends the beta-user proxy to track both the total number of requests (using a global static Mutex<usize>) and the number of beta requests (using a field on MyProxy):
use std::sync::Mutex;
// global counter
static REQ_COUNTER: Mutex<usize> = Mutex::new(0);
pub struct MyProxy {
// counter for the service
beta_counter: Mutex<usize>, // AtomicUsize works too
}
pub struct MyCtx {
beta_user: bool,
}
fn check_beta_user(req: &pingora_http::RequestHeader) -> bool {
// some simple logic to check if user is beta
req.headers.get("beta-flag").is_some()
}
#[async_trait]
impl ProxyHttp for MyProxy {
type CTX = MyCtx;
fn new_ctx(&self) -> Self::CTX {
MyCtx { beta_user: false }
}
async fn request_filter(
&self,
session: &mut Session,
ctx: &mut Self::CTX,
) -> Result<bool> {
ctx.beta_user = check_beta_user(session.req_header());
Ok(false)
}
async fn upstream_peer(
&self,
_session: &mut Session,
ctx: &mut Self::CTX,
) -> Result<Box<HttpPeer>> {
let mut req_counter = REQ_COUNTER.lock().unwrap();
*req_counter += 1;
let addr = if ctx.beta_user {
let mut beta_count = self.beta_counter.lock().unwrap();
*beta_count += 1;
info!("I'm a beta user #{beta_count}");
("1.0.0.1", 443)
} else {
info!("I'm an user #{req_counter}");
("1.1.1.1", 443)
};
let peer = Box::new(HttpPeer::new(addr, true, "one.one.one.one".to_string()));
Ok(peer)
}
}
There is nothing special about how Pingora exposes this — any thread-safe Rust mechanism works. The proxy struct itself is shared across all concurrent requests, so any mutable state on it must be protected with a Mutex, RwLock, or an atomic type.
For high-throughput counters, prefer std::sync::atomic::AtomicUsize over Mutex<usize> — it avoids lock contention entirely and compiles down to a single CPU instruction.
Connection Count Tracking
The connected_to_upstream() phase receives a reused: bool flag indicating whether the connection was pulled from the pool or freshly established. You can store this in CTX and use it in logging() to emit per-request metrics:
pub struct MyCtx {
upstream_reused: bool,
}
async fn connected_to_upstream(
&self,
_session: &mut Session,
reused: bool,
_peer: &HttpPeer,
#[cfg(unix)] _fd: std::os::unix::io::RawFd,
#[cfg(windows)] _sock: std::os::windows::io::RawSocket,
_digest: Option<&Digest>,
ctx: &mut Self::CTX,
) -> Result<()> {
ctx.upstream_reused = reused;
Ok(())
}
async fn logging(
&self,
session: &mut Session,
_e: Option<&Error>,
ctx: &mut Self::CTX,
) {
info!(
"request complete — upstream connection reused: {}",
ctx.upstream_reused
);
}
You can run the full context example from the Pingora repository with:
RUST_LOG=INFO cargo run --example ctx