Deployment¶
The built-in server is for development¶
app.run() starts Veloce's own HTTP server. It is convenient for local
development — one call, no extra dependency — but it is not intended
to face production traffic, and run() logs a reminder to that effect
on startup.
For production, run the app under a hardened ASGI server. Veloce is a
plain ASGI application (it implements __call__(scope, receive, send)),
so any ASGI server works:
Uvicorn (or Hypercorn, Granian, …) brings battle-tested HTTP/1.1 and WebSocket handling, worker management, graceful reloads, and TLS. The built-in server covers the development inner loop; production should not depend on it.
What the built-in server does and doesn't do¶
The development server does apply a slowloris guard — once a
request's bytes start arriving, the whole request must complete within
HttpProtocol.REQUEST_TIMEOUT seconds (default 30) or the connection is
dropped with 408 — and an idle keep-alive timeout.
It serves HTTP/1.1 only — it performs no WebSocket upgrade handshake and does not implement HTTP/2. Run WebSocket routes and HTTP/2 workloads under uvicorn, which implements both; Veloce's WebSocket support is reached through the ASGI server, not the built-in development server. This is a deliberate scope line: hardening a from-scratch production server is not the project's goal when mature ASGI servers already exist.
Running with multiple workers¶
uvicorn --workers N (or several app.run() processes) forks N
independent processes. They share no Python memory, which has a direct
consequence for any state Veloce keeps in-process:
| State | Shared across workers? | Notes |
|---|---|---|
Signed session cookies (SessionMiddleware) |
Yes | The session lives in the client's cookie; any worker can verify it with the shared secret. Safe across workers. |
RateLimitMiddleware buckets |
No | Each worker counts only the requests it served, so the effective limit is N × the configured value. |
g / request.state |
n/a | Per-request, never shared — correct by construction. |
app.state / app.config |
No (after fork) | Mutating app.state at runtime affects only the worker that did it. |
Veloce.mount-ed in-memory data, module globals |
No | Per process. |
Guidance:
- Sessions — signed cookies are stateless, so multi-worker is fine.
For server-side, revocable sessions use
ServerSessionMiddlewarewith aSessionStorebackend. The bundledInMemorySessionStoreis per-process, so a multi-worker deployment needs a shared store (Redis, a database) implementing theSessionStoreinterface. - Rate limiting —
RateLimitMiddlewareis in-memory and therefore per-worker. For an accurate global limit across workers, put the limiter in a reverse proxy (nginxlimit_req) or back it with Redis. - Any in-memory cache or counter — assume it is per-worker. Move anything that must be globally consistent into an external store.
A single-worker deployment behind a reverse proxy sidesteps all of the above; scale out with more workers only once shared state is externalised.
Security considerations¶
Hardening checklist¶
- Call
app.use_secure_defaults()(secure session cookies +SecurityHeadersMiddleware) for any internet-facing deployment. - Run
veloce check your_module:appbefore deploying — it flagsDEBUG, a missingSECRET_KEY, insecure session cookies, and missing security headers. - Set
MAX_CONTENT_LENGTHso oversized uploads are refused. - Keep
debug=Falsefor anything reachable beyondlocalhost— debug tracebacks leak source and internals.
Regex constraints and ReDoS¶
A pattern= (or regex=) constraint on Query, Path, Header,
Cookie, or Form is compiled once and then matched synchronously on
the event loop against the request value. A pattern with catastrophic
backtracking — typically nested quantifiers like (a+)+, (.*)*, or
overlapping alternations — can take super-linear time on a crafted
input, stalling the loop and every other request on that worker
(a regular-expression denial of service, ReDoS).
Guidance for developer-supplied patterns:
- Prefer simple, linear-time patterns; avoid nested quantifiers and quantified groups that can match the same text more than one way.
- Anchor patterns (
^…$) so the engine does not retry at every offset. - Pair a pattern with a
max_lengthconstraint so the input the regex runs against is bounded. - Test any non-trivial pattern against a deliberately adversarial input before shipping it.
Veloce compiles each pattern once at route registration, so the compile cost is paid up front — but the match cost is still the developer's responsibility to keep linear.