Architecture

How Pounce's server, supervisor, and worker layers fit together

3 min read 594 words

Overview

Pounce follows a three-layer architecture: Server orchestrates lifecycle, Supervisor manages workers, and Workers handle requests. All layers share a single frozenServerConfig— no synchronization needed.

flowchart TD Server["Server\nCONFIG → BIND → SERVE"] Supervisor["Supervisor\ndetect nogil → threads\ndetect GIL → processes"] Server --> Supervisor Supervisor -- "spawn N workers" --> W1["Worker 1\nasyncio event loop"] Supervisor -- "spawn N workers" --> W2["Worker 2\nasyncio event loop"] Supervisor -- "spawn N workers" --> WN["Worker N\nasyncio event loop"] W1 --> Shared["Shared Immutable State\n(config, app reference)"] W2 --> Shared WN --> Shared

Server Layer

TheServerclass orchestrates the full lifecycle:

  1. CONFIG — Validate and freezeServerConfig
  2. DETECT — Check for free-threading viasys._is_gil_enabled()
  3. BIND — Create listening sockets withSO_REUSEPORT
  4. LIFESPAN — Run ASGI lifespan protocol (startup / shutdown)
  5. SERVE — Delegate to single-worker fast path or multi-worker supervisor
  6. SHUTDOWN — Graceful connection draining, signal handling

For single-worker mode (workers=1), the server skips the supervisor entirely and runs the worker directly — no thread/process overhead.

Supervisor Layer

TheSupervisormanages worker lifecycle:

  • Spawn — Creates N worker threads (nogil) or processes (GIL)
  • Monitor — Health-check loop with automatic restart (max 5 restarts per 60s window)
  • Reload — Graceful restart of all workers when--reloaddetects changes
  • Shutdown — Signals all workers to stop, waits for connection draining

Worker Layer

EachWorkerruns its own asyncio event loop:

  1. Accept — Receive TCP connections from the shared socket
  2. Parse — Feed bytes to protocol parser (h11, h2, or wsproto)
  3. Bridge — Build ASGI scope, createreceive/sendcallables
  4. Dispatch — Callapp(scope, receive, send)
  5. Respond — Serialize response and write to socket (streaming)

Workers are fully independent. No shared mutable state, no locks, no coordination between workers during request handling.

Request Pipeline

A single HTTP request flows through:

flowchart LR A[Socket Accept] --> B[TLS Unwrap] B --> C{Protocol\nDetection} C -->|h1| D1[h11 / httptools] C -->|h2| D2[h2] D1 --> E[ASGI Scope\nConstruction] D2 --> E E --> E2[Proxy Header\nValidation] E2 --> E3[Request ID\nGeneration] E3 --> E4{Health\nCheck?} E4 -->|yes| E5[Health Response] E4 -->|no| F["app(scope, receive, send)"] F --> G[Response\nSerialization] G --> G2[CRLF\nSanitization] G2 --> H[Compression\nzstd / gzip / identity] H --> I[Server-Timing\nHeader Injection] I --> J[Socket Write]

The bridge is per-request — created and destroyed within a single connection handler. This ensures zero cross-request state leakage.

Module Map

Module Layer Purpose
server.py Server Lifecycle orchestration
supervisor.py Supervisor Worker spawn/monitor
worker.py Worker asyncio loop, request handling
config.py Shared FrozenServerConfig
protocols/h1.py Protocol HTTP/1.1 via h11
protocols/h1_httptools.py Protocol HTTP/1.1 via httptools
protocols/h2.py Protocol HTTP/2 via h2
protocols/ws.py Protocol WebSocket via wsproto
asgi/bridge.py Bridge HTTP ASGI scope/receive/send
asgi/h2_bridge.py Bridge HTTP/2 ASGI bridge
asgi/ws_bridge.py Bridge WebSocket ASGI bridge
asgi/lifespan.py Bridge ASGI lifespan protocol
net/listener.py Network Socket bind, SO_REUSEPORT, UDS
net/tls.py Network TLS context creation
_proxy.py Security Proxy header validation
_request_id.py Observability Request ID generation/extraction
_health.py Observability Built-in health check endpoint
metrics.py Observability Prometheus-compatible metrics

See Also