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
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.
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_pathtells 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 tool | Why it matters | Common mistake |
|---|---|---|
--proxy-headers | interpret scheme and client address | leaving it off behind a proxy and misreading URL or scheme |
--forwarded-allow-ips | restrict which hops are trusted | using * casually |
TrustedHostMiddleware | constrain accepted Host headers | allowing arbitrary hosts |
HTTPSRedirectMiddleware | redirect plain HTTP to HTTPS | duplicating behavior already handled at ingress |
3) Health endpoints are not all the same
The Kubernetes docs distinguish probe roles clearly.
| Probe | Question | Meaning of failure |
|---|---|---|
| liveness | is the process dead or stuck | candidate for restart |
| readiness | should traffic reach this instance right now | remove from service endpoints |
| startup | has initialization finished yet | delay 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:
- mark readiness as failed so new traffic stops arriving
- allow in-flight requests and connections some time to finish
- close engines, clients, and consumers in lifespan shutdown
- 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
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
For runnable intuition, pair this chapter with examples/uvicorn_proxy_and_health_lab.py.