Architecture
🧱 Components
Section titled “🧱 Components”Pingclair is a Cargo workspace. The running server is the pingclair binary,
which links the crates below.
| Crate | Responsibility |
|---|---|
pingclair |
Command-line entry point: argument parsing, logging, startup, and the service wrapper. |
pingclair-config |
Configuration compiler: lexes, parses, and semantically checks the Pingclairfile. |
pingclair-proxy |
HTTP/1.1 and HTTP/2 proxying on Pingora, the HTTP/3 listener on quiche, load balancing, and the shared request policy layer. |
pingclair-static |
Static file serving: file reads, MIME types, range requests, and streaming. |
pingclair-tls |
Certificate management: manual certificates, a persistent internal certificate authority, and automatic ACME issuance. |
pingclair-api |
Admin API for inspecting state and reloading configuration. |
pingclair-core |
Data structures and server lifecycle shared by the crates above. |
🚦 The path of a request
Section titled “🚦 The path of a request”client | | TLS with ALPN, or QUIC vlistener HTTP/1.1 and HTTP/2 on TCP, HTTP/3 on UDP | vtransport adapter Pingora ProxyHttp for TCP, tokio-quiche for QUIC | vpolicy layer routing, matchers, headers, rate limits, access log | vhandler file server | reverse proxy | FastCGI | static response | vupstream or diskBoth transports converge on the same policy layer, so routing, header handling, rate limiting, and access logging behave the same on HTTP/1.1, HTTP/2, and HTTP/3. The transports differ only where the protocol requires it, and those differences are listed below.
🌊 Request handling properties
Section titled “🌊 Request handling properties”- Bodies are streamed. Request and response bodies move through the proxy in bounded chunks. Compression, middleware, and proxying do not buffer a complete body, so a large upload or a slow reader does not consume memory proportional to the body size.
- Upstream connections are pooled. Keepalive connections to backends are
reused. Hostname upstreams are re-resolved on the interval set by
dns_refresh, so a container that restarts on a new address is followed without an operator action. - Runtime state is immutable at request time. Requests read a published snapshot. A reload publishes a new snapshot instead of mutating the one in use.
🌐 Protocol-specific behavior
Section titled “🌐 Protocol-specific behavior”Some behavior differs by protocol by design. It is listed here rather than discovered later:
| Area | Behavior |
|---|---|
| Trailers | Declared request trailers are not forwarded. The server answers 501 before the response is committed, resets an already committed HTTP/3 stream, and answers 502 when an upstream advertises response trailers. |
| CONNECT | CONNECT and extended CONNECT return 501 on HTTP/3 until tunnel support is implemented. |
| FastCGI | php_fastcgi works on HTTP/1.1 and HTTP/2. Routes that need FastCGI return 501 on HTTP/3 until that path has its own FastCGI client. |
⚠️ Known defect
Section titled “⚠️ Known defect”WebSocket upgrades fail intermittently under load: roughly 10-15% of upgrades on
a busy machine. The cause is a race in the upstream pingora-proxy crate rather
than in Pingclair’s own upgrade handling, and it is invisible on an idle
developer machine, which is why it is documented here. Upstream issue:
cloudflare/pingora#946.
