Skip to content

Server entrypoints

serve

serve(app: Application, config: Config | None = None) -> None

Start the server. This is the primary programmatic entrypoint and matches the behavior of the h2corn CLI. For an in-process lifecycle, use Server.

On Unix, the call goes through the multi-worker supervisor, even for workers=1, so signal handling, inherited listeners, and worker lifecycle stay identical to the CLI. On Windows, a single in-process worker is used.

Blocks until the server is asked to shut down via one of:

SignalEffect
SIGINT / SIGTERMGraceful shutdown
SIGHUPRolling worker reload
SIGTTINScale workers up by one
SIGTTOUScale workers down by one

Parameters:

NameTypeDescriptionDefault
appApplication

An ASGI 3 application callable.

required
configConfig | None

Server configuration. Defaults to Config() (one worker, bound to 127.0.0.1:8000).

None

Examples:

examples/embedded.py shows the Server alternative: one worker inside an event loop you already own, rather than the supervisor this starts.

Server

Server(app: Application, config: Config | None = None)

In-process, single-worker ASGI server.

Use this when you want to embed h2corn inside an existing event loop — for example, inside a test harness, a custom CLI, or an application that manages its own process model. For ordinary deployments, prefer serve, which goes through the multi-worker supervisor and matches the h2corn CLI.

The configured bind listeners are opened, lifespan startup runs, then the server processes requests until shutdown() is called or the surrounding task is cancelled.

Config describes a whole deployment, so some of its settings belong to the supervisor that runs several workers rather than to one server. workers is refused outright — asking for eight and getting one is not a detail — while max_requests, max_requests_jitter and timeout_worker_healthcheck only say when a supervisor would replace a worker, so serve() warns and carries on.

Examples:

examples/embedded.py runs one worker on an ephemeral port, sends a request through it, and shuts it down.

addressesproperty

addresses: tuple[str, ...]

Resolved listener addresses, in Config.bind string form.

Empty until serve() has bound the listeners. Unlike Config.bind, these carry the port the kernel actually assigned — bind to port 0 and read the address back from here:

Illustrative (not executable):

server = Server(app, Config(bind=('127.0.0.1:0',)))
serving = asyncio.create_task(server.serve())
await server.wait_started()
host, _, port = server.addresses[0].rpartition(':')

releasingproperty

releasing: bool

Whether a previous serve() is still releasing its requests.

serve() stops waiting once the graceful budget is spent, but requests that ignored cancellation keep the application and its loop shards alive. While this is true the same Server cannot serve() again. Lifespan shutdown runs only after those requests finally release.

serveasync

serve() -> None

Run the server until shutdown.

Binds the configured listeners, runs ASGI lifespan startup, processes requests, then runs lifespan shutdown after the native drain has fully released the application.

One lifecycle at a time, reusable afterwards: once a serve() call has fully finished, the same Server can serve() again with fresh shutdown state, while a second concurrent call raises RuntimeError. Cancelling the serve() task does not abort in-flight work — it triggers the same bounded graceful drain as shutdown() and re-raises the cancellation once that drain finishes or its budget runs out.

The budget is real: an application that catches CancelledError and never finishes cannot hold serve() open. serve() raises RuntimeError instead, naming the problem. Lifespan shutdown still runs after those requests finally release; until then the same Server cannot serve() again.

Raises NotImplementedError if Config.workers is not 1; multi-worker deployments must go through serve.

shutdown

shutdown() -> None

Initiate a graceful shutdown of an in-flight serve() call.

Safe to call from any thread or coroutine. Native acceptance stops immediately and cooperative tasks receive up to Config.timeout_graceful_shutdown seconds to finish. Arbitrary synchronous Python code cannot be preempted in-process; use supervised workers when a hard kill bound is required.

wait_startedasync

wait_started() -> None

Wait until this server is accepting connections.

Resolves once the listeners are live, so a caller that starts serve() as a task can await this instead of watching addresses for one to appear.

Raises rather than waiting when waiting cannot end well: whatever stopped the server if it stops without ever serving, and RuntimeError if it is already shutting down — a server on its way down is not one that is about to come up.

Illustrative (not executable):

serving = asyncio.create_task(server.serve())
await server.wait_started()
# the port is open here