On this page
API reference for fastware server: Granian ASGI lifecycle management with foreground and background serving, PID files, port checks, and hot reload.
#Server API Reference
The server module manages the Granian ASGI server lifecycle: PID file management, port availability checks, single-instance enforcement, background and foreground serving, hot reload, and graceful shutdown.
Server symbols are lazily imported from the top-level fastware package to avoid the ~60ms cost of importing Granian when only the routing/response layer is needed.
#src.fastware.server
Granian ASGI server lifecycle management with PID file tracking, port availability checks, foreground and background serve modes, and graceful stop.
#PortInUseError
Raised when the requested port is already in use.
#AlreadyRunningError
Raised when a single-instance server is already running.
#_write_pid
def _write_pid(pid_path: Path) -> NoneWrite the current PID to disk, become process group leader, and register cleanup handlers.
Becoming a process group leader (via os.setpgid(0, 0)) lets the 'stop' subcommand signal our entire process group with os.killpg, so granian worker subprocesses die with us instead of being orphaned.
#_remove_pid
def _remove_pid(pid_path: Path) -> NoneRemove the PID file if it exists.
#_signal_handler
def _signal_handler(signum: int, _frame: object, pid_path: Path) -> NoneHandle SIGTERM/SIGINT: forward to our process group, clean up, exit.
#check_already_running
def check_already_running(pid_path: Path, name: str='server') -> int | NoneCheck if another instance is running. Returns the PID if running, None otherwise.
Stale PID files (process dead) are cleaned up automatically.
#ensure_port_available
def ensure_port_available(host: str, port: int, name: str='server') -> intEnsure port is available. If occupied by a previous instance, stop it.
Probes the port. If something responds to GET /health with {"status":"ok"}, it's likely our own stale server -- kill it via the OS. Otherwise, exit with a diagnostic error.
Returns port on success.
#_kill_port_holder
def _kill_port_holder(host: str, port: int) -> NoneKill the process holding a port.
#_resolve_target
def _resolve_target(target: str | Callable) -> strConvert a target to a Granian-compatible string.
If target is already a string (e.g. "myapp:app"), return as-is. If target is a callable, register it on a synthetic module so Granian can import it via its string-based loader.
#_find_free_port
def _find_free_port(host: str='127.0.0.1') -> intFind an ephemeral port that is currently free on host.
#_port_file_path
def _port_file_path(pid_path: Path) -> PathDerive the port file path from a PID file path.
E.g. .pixelweaver.pid -> .pixelweaver.port.
#_write_port_file
def _write_port_file(pid_path: Path, port: int) -> NoneWrite the bound port alongside the PID file.
#_remove_port_file
def _remove_port_file(port_path: Path) -> NoneRemove a port file if it exists.
#read_port_file
def read_port_file(pid_path: Path) -> int | NoneRead the port stored alongside a PID file. Returns None if missing or unreadable.
#_resolve_host_port
def _resolve_host_port(host: str | None, port: int | None, name: str) -> tuple[str, int]Resolve host and port from explicit args or env vars.
Explicit args override env vars. If neither is provided, raise ValueError.
#_make_server
def _make_server(target: str, host: str, port: int) -> GranianCreate a Granian instance bound to host on port.
target is an ASGI module path, e.g. "myapp:app".
#_run_server
def _run_server(target: str, host: str, port: int) -> NoneCreate and run a Granian server. Used as the reload subprocess target.
#_serve_subprocess
def _serve_subprocess(target: str, host: str, port: int, pid_path_str: str, name: str) -> NoneEntry point for the server subprocess. Runs granian in foreground mode.
#serve_background
def serve_background(target: str | Callable, *, host: str, port: int, pid_path: Path, name: str='FASTWARE') -> strStart the server as an independent background process. Returns the URL.
Unlike serve(foreground=False) which uses a daemon thread (dies with the parent), this spawns a fully detached subprocess that survives the parent exiting. The subprocess writes its own PID and port files.
Parameters
target: ASGI application -- either a module path string (e.g. "myapp:app") or a callable ASGI application object. host: Bind address. port: Bind port. Pass 0 to pick a random free port. pid_path: Path for the PID file. The subprocess writes this, not the caller. name: Application name for log messages.
Returns
str The URL the server is listening on (e.g. "http://127.0.0.1:8000").
Raises
RuntimeError If the server process exits prematurely or fails to start within 10s.
#serve
def serve(target: str | Callable, *, foreground: bool, host: str | None=None, port: int | None=None, pid_path: Path | None=None, name: str='FASTWARE', pre_serve: Callable[[], None] | None=None, reload: bool=False, single_instance: bool=True) -> str | NoneStart Granian serving the ASGI app.
Parameters
target: ASGI application -- either a module path string (e.g. "myapp:app") or a callable ASGI application object. foreground: Required. When True, blocks the calling thread. When False, spawns a daemon thread and returns the URL string. host: Bind address. Falls back to {NAME}_HOST env var. ValueError if neither. port: Bind port. Falls back to {NAME}_PORT env var. ValueError if neither. pid_path: If given, enables PID file management (detect existing instances, write PID, register cleanup). name: Application name for env var prefix (uppercased) and log messages. Default "FASTWARE". pre_serve: Optional callable invoked synchronously after PID/port checks but before Granian starts. reload: When True, watches .py files in the current working directory and restarts the server on changes. Requires foreground=True. single_instance: When True (default), checks for an existing running instance via the PID file and exits with an error if one is found. When False, skips the PID check (but still writes the PID file if pid_path is given).
Returns
str | None When foreground=False, returns the URL string (e.g. "http://127.0.0.1:8000"). When foreground=True, returns None (blocks until server stops).
#ServerStatus
Result of a status check on a server process.
#_cleanup_pid_and_port
def _cleanup_pid_and_port(pid_path: Path) -> NoneRemove both the PID file and its companion port file.
#stop
def stop(pid_path: Path) -> NoneStop a server by reading its PID file.
Sends SIGTERM, waits up to 10s polling with os.kill(pid, 0), then escalates to SIGKILL. Cleans up the PID file and port file.
Raises FileNotFoundError if PID file does not exist. If the process is already gone (stale PID file), removes the PID file and returns normally.
#status
def status(pid_path: Path, health_url: str | None=None) -> ServerStatusCheck the status of a server process.
Parameters
pid_path: Path to the PID file. health_url: Optional URL to probe for health (e.g. "http://127.0.0.1:8000/health"). If provided and the process is running, an HTTP GET is attempted with a short timeout.
Returns
ServerStatus Dataclass with running, pid, and healthy fields.
#ServerStatus
The ServerStatus enum represents the 3 possible states of a fastware server instance: running, stopped, or unknown. It is returned by status() after checking the PID file and probing the process.
| Field | Type | Default | Description | |
|---|---|---|---|---|
running | bool | |||
pid | int | None | ||
healthy | bool | None |
#Error Types
#PortInUseError
Raised by ensure_port_available when the requested port is already in use and cannot be reclaimed. The error message includes the host and port, and suggests using a different port or stopping the process holding it.
Before raising, ensure_port_available attempts to detect whether the port holder is a stale instance of the same server (by probing GET /health). If it is, the stale process is killed automatically and the port is reclaimed without error.
#AlreadyRunningError
Raised by serve (when single_instance=True) if a PID file exists and the corresponding process is still alive. The error message includes the PID of the running instance. This prevents accidentally starting duplicate servers on the same port, which would cause bind failures or silent request splitting.
Stale PID files (where the process has died) are cleaned up automatically and do not trigger this error.
#serve() vs serve_background()
serve() is the primary entry point for starting the Granian ASGI server. It supports both foreground (blocking) and background (daemon thread) modes, with optional PID file management, single-instance enforcement, and hot reload for development workflows:
from fastware.server import serve
# Foreground mode -- blocks until the server stops
serve(
"myapp:app",
foreground=True,
host="127.0.0.1",
port=8000,
pid_path=Path(".myapp.pid"),
)
# Background mode -- returns the URL, server runs in a daemon thread
url = serve(
"myapp:app",
foreground=False,
host="127.0.0.1",
port=8000,
)
# url == "http://127.0.0.1:8000"serve_background() spawns a fully detached subprocess that survives the parent process exiting. Use this when you need the server to outlive the caller (e.g., starting a server from a CLI command):
from fastware.server import serve_background
# Detached subprocess -- survives parent exit
url = serve_background(
"myapp:app",
host="127.0.0.1",
port=8000,
pid_path=Path(".myapp.pid"),
)| Feature | serve(foreground=False) | serve_background() |
|---|---|---|
| Process lifetime | Dies with parent (daemon thread) | Survives parent exit (subprocess) |
| Use case | Desktop apps, test harnesses | CLI start/stop commands |
| Returns | URL string | URL string |
| PID file | Optional | Required |
#Hot Reload
Pass reload=True to serve() for development. This uses watchfiles to monitor all .py files in the project directory and automatically restart the Granian server on changes, providing sub-second feedback during development. Requires foreground=True:
serve(
"myapp:app",
foreground=True,
host="127.0.0.1",
port=8000,
reload=True,
)