Skip to content

Proxy, Health, and Shutdown

A service that works locally with `uvicorn main:app --reload` can still behave differently in production because the operational boundary is different. Reverse proxies, `root_path`, forwarded headers, readiness, and graceful shutdown are where many "the code looks right, but production is odd" problems actually come from.

Quick takeaway: behind a proxy, `root_path`, trusted forwarded headers, and correct host or scheme handling matter. In environments such as Kubernetes, liveness, readiness, and startup probes should have distinct roles. Graceful shutdown is best modeled as "become unready, stop accepting new traffic, drain and clean up, then exit".

The full picture

Production services more often sit behind proxies and orchestrators than they sit directly on the public edge.

1) Understand root_path behind a reverse proxy

Suppose the proxy exposes the app under /api/v1, while the app code itself still declares routes such as /users. In ASGI, root_path is the mechanism that communicates that external mount prefix.

py
from fastapi import FastAPI, Request

app = FastAPI(root_path="/api/v1")


@app.get("/users")
def read_users(request: Request) -> dict[str, str]:
    return {
        "path": request.scope["path"],
        "root_path": request.scope.get("root_path", ""),
    }

Important points:

  • root_path tells the app about the external path prefix
  • FastAPI docs UI and generated OpenAPI server URLs are affected by it
  • Uvicorn does not magically understand the external prefix by itself; the proxy handles the public path while the app receives the ASGI root_path

2) Only trust forwarded headers from proxies you actually trust

According to the Uvicorn docs, --proxy-headers and --forwarded-allow-ips control how headers such as X-Forwarded-Proto and X-Forwarded-For are interpreted. Those headers can be forged, so you should only trust them from proxy hops you control.

Setting or toolWhy it mattersCommon mistake
--proxy-headersinterpret scheme and client addressleaving it off behind a proxy and misreading URL or scheme
--forwarded-allow-ipsrestrict which hops are trustedusing * casually
TrustedHostMiddlewareconstrain accepted Host headersallowing arbitrary hosts
HTTPSRedirectMiddlewareredirect plain HTTP to HTTPSduplicating behavior already handled at ingress

3) Health endpoints are not all the same

The Kubernetes docs distinguish probe roles clearly.

ProbeQuestionMeaning of failure
livenessis the process dead or stuckcandidate for restart
readinessshould traffic reach this instance right nowremove from service endpoints
startuphas initialization finished yetdelay liveness and readiness checks

Practical guidance

  • keep liveness checks lightweight
  • let readiness reflect DB, cache, warmup, or drain state
  • use startup probes for slow-starting services so liveness does not restart them too early

4) Model graceful shutdown as "drain, clean up, exit"

A good shutdown sequence:

  1. mark readiness as failed so new traffic stops arriving
  2. allow in-flight requests and connections some time to finish
  3. close engines, clients, and consumers in lifespan shutdown
  4. enforce timeout policy if work does not finish within the grace period

Common mistakes:

  • killing the process immediately on SIGTERM
  • never connecting readiness state to shutdown behavior
  • forgetting long-lived websocket or streaming connections
  • leaving long-running background work inside the API process

5) Middleware can add operational guardrails

py
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware

app.add_middleware(TrustedHostMiddleware, allowed_hosts=["api.example.com", "*.example.com"])
app.add_middleware(HTTPSRedirectMiddleware)

These are app-level guardrails for host and scheme handling. But if ingress or the load balancer already owns those concerns, double redirects or contradictory behavior are easy to introduce, so align the layers deliberately.

6) Operational checklist

Verify `root_path`

If the proxy adds a path prefix, make sure docs URLs and generated server URLs still match the public path.

Constrain trusted proxies

Only trust forwarded headers from actual proxy hops you control, otherwise client or scheme spoofing becomes possible.

Separate probe roles

Liveness answers "am I alive", readiness answers "should I receive traffic", and startup answers "am I ready to begin checks".

Support drain before exit

Drop readiness before shutdown so new traffic stops, then give long-lived work time to close cleanly.

Companion chapters

  1. ASGI and Uvicorn
  2. WebSockets, Streaming, and Middleware
  3. Lambda vs Kubernetes

For runnable intuition, pair this chapter with examples/uvicorn_proxy_and_health_lab.py.

Official References

Built with VitePress for a Python 3.14 handbook.