Production
Run Python Serve as a hardened standalone process, with per-process budgets wherever it is served.
A ProductionProfile sets process budgets: admission, request deadlines and result sizes. start_server runs an API
created with one as a hardened standalone process:
from hypequery.serve import HttpSecurity, ProductionProfile, create_api, start_server
api = create_api(
auth=authenticate,
client=client,
datasets={"orders": orders},
security=HttpSecurity(
allowed_hosts=("analytics.example.com",),
trusted_proxies=("127.0.0.1",),
),
production=ProductionProfile(
max_concurrency=32,
timeout_seconds=30,
max_result_rows=10_001,
max_result_bytes=8 << 20,
),
)
if __name__ == "__main__":
start_server(api)start_server needs the server extra (pip install "hypequery[server]==0.1.0b2"). run_production is the same function.
To close an executor at shutdown, pass the ASGI app with a lifespan:
start_server(to_asgi_app(api, lifespan=...)).
Process
- Binds to
127.0.0.1:8000by default (ProductionProfile(host=..., port=...)). Binding outside loopback, for example inside a container, requiresallow_external_bind=True. - Runs one uvicorn worker per process with reload, access logging, server headers, and WebSockets disabled. Use a process manager to run several instances; limits apply per instance.
- Terminate TLS at a reverse proxy and list its addresses in
HttpSecurity.trusted_proxies. Production rejects all-address networks, and uvicorn's own forwarded-header handling is turned off so proxy trust is decided once. create_apirefuses a production profile without anHttpSecurity, with development docs, or withfrom_context(..., same_origin=False). A header credential (bearer, API key, JWT) is never read from a cookie; your app's session is read only by afrom_contextstrategy, with its same-origin check.hypequery devrefuses to run a production API.
Limits
| Setting | Default | Effect |
|---|---|---|
max_concurrency | 32 | Excess in-flight requests get 503 SERVICE_UNAVAILABLE without queuing |
timeout_seconds | 30 | Covers auth, body, query, and response; expiry cancels the query and returns 504 |
max_result_rows | 10,001 | Includes the one-row pagination probe, so at most 10,000 rows are returned |
max_result_bytes | 8 MiB | Applies to ClickHouse results and the serialized response; oversized bodies get 413 |
max_threads | 4 | ClickHouse thread ceiling per query |
Time, row, byte, and thread ceilings are passed into the compiled query and intersected with stricter client settings. Responses are buffered under the byte ceiling, so this profile does not provide streaming exports.
A request that times out keeps its admission slot until the work it started has stopped, so timed-out requests can't pile up. Work still waiting for a worker thread is cancelled before your code starts. Synchronous executors must honor the cancellation signal: the ClickHouse executor does, through its control connection, but Python cannot stop arbitrary code already running in a thread.
Mounted in another app
The profile's budgets apply wherever the API is served, including when it is mounted in another application or served through the Django and Flask adapters. The host application owns its own process settings and safeguards for everything else it serves.