The built-in server¶
Use this when you run your app on Linux or macOS and want the fastest setup, without installing an ASGI server.
Korvex runs an app without an ASGI server, on Linux and macOS:
korvex run main:app --workers 3
or, from Python, app.run(workers=3). On the same CPU cores, it serves 21 to
32% more requests than uvicorn, with 40% less memory (see
Performance).
The app stays a plain ASGI app: uvicorn, Granian or a serverless platform run it too, and Windows needs one of them.
Options¶
The same in korvex run and app.run(). In Python, use underscores and
pass lists as lists:
app.run("0.0.0.0", 8000, workers=3, graceful_timeout=10, forwarded_allow_ips=["10.0.0.5"])
| Option | Default | Does |
|---|---|---|
--host, --port |
127.0.0.1, 8000 |
Address to listen on. |
--uds PATH |
Listen on a Unix socket instead, for a proxy on the same machine. | |
--workers N |
1 |
Worker processes sharing the port; about 3 per 4 CPU cores works best. A worker that crashes is restarted. |
--max-connections N |
0 (no limit) |
Open connections per worker and thread (WebSockets count). Beyond it, new clients wait in the listen backlog, not refused, until one closes. Memory is bounded by about N × --max-body. |
--max-requests N |
0 (never) |
A worker stops gracefully after about N HTTP requests (N plus a random 0 to 10%, so workers don't restart together) and a new one replaces it at once: a cap on slow memory leaks. Counted per worker, all its threads together; WebSocket handshakes aren't counted. |
--threads N |
1 |
Event loops per worker, each in its own thread: see below. |
--graceful-timeout S |
30 |
On Ctrl+C or SIGTERM, seconds requests in progress get to finish. |
--max-body BYTES |
1 MiB | Larger bodies get a 413, larger WebSocket messages close the connection. A route can set its own: @app.post("/upload", max_body=50_000_000). |
--read-timeout S |
30 |
Seconds a client gets to send the headers, then the body. Slow headers (or an idle connection) are closed with no response; a slow body gets a 408. |
--forwarded-allow-ips |
127.0.0.1,::1 |
Proxies whose X-Forwarded-For and -Proto (or Forwarded) give the real client and scheme. * trusts any. |
--forwarded-host |
off | Also take the host they forward. Only if your proxy sets that header itself: nginx passes a client's X-Forwarded-Host on as it is. |
--root-path PREFIX |
none | The root_path: the prefix a proxy strips before forwarding (nginx location /api/ { proxy_pass http://app/; }), such as /api. Empty, or starting with / and not ending with one. The proxy strips the prefix, so the server routes the path it receives as is, never stripping again. It only sets request.root_path and puts the prefix back in links (url_for). |
--access-log |
off | Log each request. |
--ws-ping-interval S |
20 |
Seconds between pings on open WebSockets; a client silent until the next ping is closed. 0 turns them off. |
Threads¶
With --threads 3, each worker runs 3 event loops, in 3 threads. On
free-threaded Python (3.14t) they run in parallel: one process uses
several cores and shares its memory. With the GIL, they take turns on one
core, so use workers instead.
Each event loop runs the startup and shutdown hooks. Keep what they open in
app.local, which holds one value per loop: a module global would give
every loop the first loop's connection pool, and asyncio objects only
work on the loop that made them.
How it works¶
A Rust thread per loop reads each request in full, without touching Python, then hands it to the asyncio loop that runs your handlers. A slow or huge upload never holds Python up.
Good to know¶
--max-requestsrecycles workers: even with one worker, the server forks it and restarts it as a supervisor would. A clean restart never triggers the crash backoff.- In production, run it behind a reverse proxy (nginx, Caddy, Traefik): it speaks HTTP/1.1 and WebSocket, and leaves HTTPS, HTTP/2 and HTTP/3 to the proxy. The deployment guide has ready configurations.
- A body that's too big (413), too slow (408) or broken (400) is still answered by the app, so before hooks, exception handlers, after hooks and the access log see it.
- A slow client can't make a stream or a WebSocket pile up in memory: once 1 MiB waits for it, the handler's next send waits.
- A client can't flood a WebSocket handler: once 1 MiB of its messages
waits for
receive(), the server stops reading from it until the handler catches up. A send-only handler learns the client left from its next send. - With several workers, a worker that crashes after starting is restarted
after a growing delay (0.1 s, doubling up to 30 s). On SIGTERM or SIGINT
the workers get
graceful_timeoutto finish, and are killed 5 s later if still there; a second signal kills them at once. - From a proxy's forwarded headers, the rightmost value counts: the one the nearest proxy set, not one the client could have written.
Next: Deploying a Korvex app, with proxy configurations, Docker and Kubernetes.