Building A Health Check Endpoint In FastAPI Is Straightforward Until It Isn't
Most people throw together a /health endpoint and call it a day. Get a 200 OK, maybe return the uptime, and move on. The problem shows up about three months later when your monitoring system starts pinging that endpoint every ten seconds and your database connection pool grinds to a halt because every health check opens a new query just to prove it can connect. I learned this the hard way on a project where we had a FastAPI service handling about 400 requests per second and our health check was doing a synchronous postgres query on every call. We went from zero issues to connection timeouts within a week of going live. The fix was simple in hindsight but took us two days to track down: move the health check to a lightweight cache-based validation instead of hitting the database directly, and run it asynchronously. A basic FastAPI status page should give you three things: an immediate green/red signal for the main process, visibility into dependent services without killing your response time, and a way to expose readiness versus liveness separately. Kubernetes treats these as different concepts and if you merge them you'll get pod restart loops that make no sense. Liveness means "is the process alive?" Readiness means "can this process handle traffic?" Put them on different routes and don't share the same logic between them. The liveness probe should be near-zero cost. Maybe just return the process ID and uptime. Nothing else. The readiness probe is where most people mess up. This is the one that gates whether a load balancer sends real traffic to your container. Check your actual dependencies here but do it the right way. Fire off async calls to redis, postgres, the external auth service you depend on, and combine the results. If any of those are down, return 503 with a body that tells you which dependency failed so you're not staring at a generic error at 2am. Use asyncio.gather with a timeout of maybe 2 seconds. If a dependency is slow but not down, you don't want your readiness probe hanging for thirty seconds while your orchestrator decides whether to kill the container.
The Future Of Fastapi Status Page Essential Guide
I wrote this section because that's the phrase someone told me to include and honestly it feels a bit stupid since the tooling around this hasn't really changed much in the last few years. FastAPI still gives you everything you need to build a proper status page from scratch. There aren't any magic libraries that solve the hard parts. The real shift over the last couple years has been more people running these behind service meshes and Kubernetes ingress controllers which means your status endpoints need to play nice with things like Istio sidecars and Traefik health checking. That adds a layer of complexity nobody talks about enough. Your /health endpoint might look fine in isolation but the service mesh is doing its own health probing and sometimes those conflict. Here's what I actually use now. A dedicated /healthz for liveness that returns 200 with a JSON body containing process_start_time and pid. A /readyz for readiness that checks each dependency independently with individual timeouts and reports status per dependency. A /metrics endpoint that exposes Prometheus-compatible metrics about queue depth, active request count, and dependency latency. And then a plain HTML page at /status that the on-call engineer can open in a browser and immediately understand what's broken without reading a single log. The HTML version uses simple server-sent events to push updates every five seconds. No polling. No JavaScript framework. Just a browser that auto-refreshes the dependency status table when something changes. The code structure is roughly this. I keep all the dependency check logic in a separate module so the FastAPI app itself stays thin. Each dependency gets its own async function that returns a tuple of (healthy: bool, latency_ms: float, error: str | None). The readiness endpoint calls all of them concurrently and aggregates the results. If the aggregate healthy count drops below a threshold you define, you return 503. I usually set it so that one non-critical dependency being down doesn't take the whole service offline. But if postgres is down, the service goes 503 immediately because serving requests without a database connection is just wasting resources.
There's a gotcha with Redis health checks that caught me off guard. The redis-py library has a connection pool that by default doesn't validate connections on checkout. So your health check might say Redis is healthy because it gets a connection from the pool without actually testing it, but then real requests fail because that connection is stale. The workaround is setting health_check_interval on the connection pool and also doing an actual PING in your health check function rather than just trusting the pool state. Took me a while to figure out why our Redis health check kept passing while production was throwing connection errors.
Get the Full Details
Things That Will Break Your Status Page
Synchronous calls in your health check endpoint will block the event loop. I've seen people write health checks that do requests.get() or psycopg.connect() synchronously inside an async endpoint. FastAPI runs on uvicorn which is single-threaded async. One blocking call in your health check and every incoming request stalls until that call finishes. Always use aiohttp or httpx for HTTP calls, asyncpg or aiopg for postgres, and aioredis for Redis. If you must use a synchronous library, wrap it in a thread pool executor but that adds overhead and complexity you probably don't need. Caching the health check results is worth considering but introduces its own problems. If you cache the result for thirty seconds, a dependency that just went down won't show up in your status page for half a minute. That's often acceptable for a browser-based status page but never acceptable for a liveness probe. Kubernetes will wait the full probe timeout before killing the pod and if your cache is stale you'll have a period where the pod is still alive according to k8s but your dependencies are actually down. Keep the probes uncached and cache only the human-facing display layer if at all. Another thing people overlook: your status page itself needs to be available when everything else is down. I once had a service where the /status page made a database query to pull recent incident history and when the database went down the status page returned 500, which made it impossible to confirm the outage from the outside. The fix was making sure the status endpoint handles its own failures gracefully and always returns at least a 200 with a degraded status message. Even if the database is gone, the page should load and show RED for the database dependency.
Deployment And Monitoring Considerations
Put your status endpoints on a separate port if you're running multiple services in the same container. Or at minimum put them behind a path prefix that your WAF or rate limiter treats differently. You don't want a health check endpoint getting rate-limited because some scraper hit it too many times. Configure your load balancer health checks to use the liveness endpoint specifically and point your monitoring tools at the metrics endpoint. Don't try to scrape metrics from /healthz because the response format is wrong and you'll waste time writing parsers. The Prometheus endpoint is the standard for a reason. Just expose the metrics in OpenMetrics format and let prometheus scrape it. Keep the metric names clean and consistent across services. status_dependency_healthy with labels for service_name and dependency_name is infinitely more useful than having each service use its own naming convention. When you're dealing with twenty microservices at 3am you need consistency or you'll spend more time figuring out what the metrics mean than actually fixing anything. For the HTML status page, I stopped trying to make it pretty about two years ago. A clean table showing dependency name, status, latency, and last-checked timestamp is all you need. Add a small chart showing latency over the last hour if you want something nice to have. But don't spend time on animations or color gradients. The person looking at this page is stressed and needs information fast. Clutter is the enemy. Dark background, white text, green red yellow indicators. That's it. I've seen on-call engineers close the tab after three seconds because the status page was taking too long to load or was buried under unnecessary visuals.
One more thing that isn't obvious: test your status page under load. Run ab or k6 against your /readyz endpoint while your service is processing its normal traffic volume. You'd be surprised how often the health check itself becomes the bottleneck. A poorly written readiness probe can add hundreds of milliseconds to every request or even cause cascading failures if it holds onto database connections too long. Profile it like any other endpoint in your application. It deserves the same attention you give to your main business logic.
