Skip to main content

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

Build docs developers (and LLMs) love