Frameworks can serve their ASGI app throughpounce.run()or construct
pounce.server.Serverdirectly. Direct construction is useful when a framework
has a fused sync callable, a lifecycle collector, or its own CLI—but it creates
an adapter contract that must stay aligned with Pounce.
The goal is not to copy everyServerConfigfield into a framework's public
configuration. The goal is to make every production-critical difference an
intentional, documented policy instead of an accidental Pounce default.
Prefer A Frozen Config Pass-Through
The smallest drift-resistant integration accepts a pre-builtServerConfig:
from pounce import ServerConfig
from pounce.server import Server
def serve_framework_app(
app,
*,
server_config: ServerConfig,
app_path: str | None = None,
sync_app=None,
lifecycle_collector=None,
) -> None:
Server(
server_config,
app,
app_path=app_path,
sync_app=sync_app,
lifecycle_collector=lifecycle_collector,
).run()
ServerConfigis frozen shared state. Build it once before workers start; do
not mutate a framework config object and expect running workers to follow.
If a framework maps its own config fields intoServerConfig, keep that
mapping in one function and test both the framework's programmatic launcher and
CLI launcher against it.
Minimum Decision Surface
“Decision” means forward a user value, accept a completeServerConfig, or
document a deliberate framework-owned fixed policy.
| Area | Pounce surface | Why an embedding framework must decide |
|---|---|---|
| Bind and workers | host, port, uds, workers, worker_mode |
Determines listener ownership and whether workers are threads, processes, async workers, or subinterpreters. |
| Application identity | Server(..., app_path=...) |
Required for subinterpreter import and for reload paths that reimport application code. A live callable alone is not importable in another interpreter. |
| Worker hooks | worker_startup_failure |
Generic ASGI compatibility defaults toignore; frameworks with required worker hooks normally need shutdownso readiness cannot succeed after hook failure. |
| Lifecycle bounds | startup_timeout, shutdown_timeout, reload_timeout |
Bounds deploy readiness, graceful shutdown, and generation rotation. Platform drain windows must be longer than Pounce shutdown bounds. |
| Connection timeouts | header_timeout, keep_alive_timeout, request_timeout, write_timeout |
Preserve the state-specific header, request-body, between-request idle, and blocked-output controls independently. HTTP/3 output liveness useshttp3_idle_timeout. |
| Request envelope | max_request_size, max_header_size, max_headers |
Pounce validates bytes before the framework sees them. A larger framework body limit is ineffective if Pounce retains its smaller default. |
| Capacity | backlog, max_connections, executor_threads_per_worker |
Bounds queued connections, live connections, and blocking work offloaded from async handlers. |
| Proxy authority | trusted_hosts, forwarded_for_trusted_hops, root_path |
Controls trusted client IP, scheme, host authority, and subpath routing before framework middleware runs. |
| TLS and protocols | ssl_certfile, ssl_keyfile, http3_enabledand protocol extras |
Defines whether Pounce or an edge owns TLS and which optional protocol handlers can be selected. |
| Operator surfaces | health/readiness, metrics, introspection, logging, lifecycle collector | A framework may own these endpoints, but collisions, redaction, and disabled Pounce paths must be deliberate. |
Feature-specific fields—static files, middleware, compression dictionaries, Sentry, OpenTelemetry, rate limiting, and request queueing—only need adapter surface when the framework advertises that Pounce-owned feature.
Constructor Inputs Are Part Of The Contract
ServerConfigis not the entire embedding API:
app_pathis the import identity for subinterpreters and code reimport.sync_appenables the fused sync path for frameworks that implement Pounce's sync callable contract. Omitting it is valid, but gives up that path.lifecycle_collectorconnects framework/operator lifecycle evidence without changing the ASGI app.
If a framework cannot supplyapp_path, it should reject
worker_mode="subinterpreter"with an actionable error rather than silently
falling back or starting an unusable worker. Pounce enforces this boundary with
POUNCE_SUPERVISOR_SUBINTERPRETER_NO_APP_PATH; explicit subinterpreter mode is
supervised even whenworkers=1.
Process-Worker Initialization Is A Fork Boundary
On a GIL build with multiple workers, Pounce usesfork. The live app and the
state produced by mainlifespan.startupare inherited by every worker. An
embedding framework must therefore finish the pre-fork phase without live
background threads, running executors, held synchronization primitives, or
process-local clients that are unsafe to inherit. Readiness cannot prove that
an inherited lock or executor will make progress when a later request uses it.
Usepounce.worker.startupto create or replace process-local resources in
each child, and setworker_startup_failure="shutdown"when that initialization
is required. If the framework cannot provide a fork-safe boundary, it should
selectworkers=1or require Python 3.14t thread workers. Pounce does not
currently provide aspawn or forkserverprocess-worker option; adding one
would require an explicit app-import, lifespan, serialization, and reload
contract rather than a silent fallback.
Framework-Owned Endpoints
A framework may own/health, /ready, metrics, or introspection. In that
case, set the overlapping Pounce endpoint to disabled and test the framework's
equivalent behavior:
- readiness turns non-200 when drain begins and before process termination;
- health and readiness paths do not collide with user routes silently;
- metrics names and paths are stable if publicly documented;
- runtime/config inspection remains redacted and off by default when exposed.
“The framework owns it” is a valid policy. Leaving the Pounce field at its default without documenting the replacement is not.
Adapter Proof Checklist
At minimum, run these through the real framework launcher and a real Pounce socket:
- A request below and above the configured body limit.
- Trusted and untrusted forwarded authority, including the configured hop count.
- Required worker startup success and failure before readiness.
- On GIL process workers, fork-safe pre-start state and per-worker resource initialization.
- SIGTERM drain bounded by the configured shutdown timeout.
- Every supported worker mode; reject unsupported modes before binding.
- Programmatic and CLI launchers produce the same
ServerConfigpolicy.
Snapshot tests of a constructor call are useful drift guards, but lifecycle, protocol, and proxy claims still need observable worker/socket tests.
Chirp Audit Snapshot
The issue that produced this guide audited Chirpmainat commit
9ada3ba4b26ed37fbfde0ef69b60c3897830d3d3on 2026-07-08.
Already aligned:
worker_mode,metrics_path,keep_alive_timeout,request_timeout, andwrite_timeout;trusted_proxies→trusted_hostsplusforwarded_for_trusted_hops;- TLS, logging, backpressure, WebSocket, OpenTelemetry, and Sentry settings;
- Chirp auth rate limiting uses the Pounce-normalized ASGI client rather than
reparsing raw
X-Forwarded-For.
Machine-verified gaps:
- Chirp's 16 MiB
max_request_body_sizeis not passed to Pounce, whose 1 MiB default rejects larger requests before Chirp sees them. - Chirp pins Pounce 0.8.2 and rejects sync worker hooks using the old Pounce 0.7 lifecycle limitation; Pounce #244 and #245 now provide sync hooks and fail-loud startup on main.
worker_startup_failure,startup_timeout,shutdown_timeout,header_timeout, andexecutor_threads_per_workerare not adapter inputs.- Chirp passes a live app without
app_pathand deliberately rejects subinterpreter mode pending an import contract.
The downstream work is tracked in Chirp #627. This is a dated audit snapshot; use Chirp's current source and issue state for the live answer.
See Also
- Production — General production setup
- Railway — Edge/origin topology and proxy trust
- ServerConfig — Complete field reference
- Core contract — Canonical ownership and proof rules