FastAPI health check endpoint
Add an async /health route that runs SELECT 1 through the session with a short timeout and returns a JSONResponse with status_code=503 when the query fails, so a live Uvicorn process with a dead database reads as down.
FastAPI makes the useless version of this endpoint very easy to write. Three lines, a dict, a 200, and you have told the world that Uvicorn is running. Uvicorn is rarely the thing that goes wrong. The connection pool behind it is. The database restarts, the pool never recovers, every route that touches a session starts raising, and the health route, which touches nothing, keeps answering in under a millisecond with the same cheerful 200 it returned yesterday.
The route has to run one real query against the dependency it cannot work without, and it has to answer with a status code that means something. A monitor watching this URL reads anything outside 200-399 as down and alerts on the change, which makes the 503 the entire point of the exercise. Keep it to one query. An endpoint that fans out to five services is an endpoint that pages you for an outage that is not yours.
The endpoint
import asyncio
from fastapi import Depends, FastAPI
from fastapi.responses import JSONResponse
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
app = FastAPI()
@app.get("/health")
async def health(session: AsyncSession = Depends(get_session)):
try:
async with asyncio.timeout(2):
await session.execute(text("SELECT 1"))
except Exception:
return JSONResponse({"status": "down"}, status_code=503)
return {"status": "ok"} Two details are load-bearing. SQLAlchemy 2.0 refuses to execute a bare string, so the text() wrapper is required or you get an ObjectNotExecutableError where you wanted a health check. And asyncio.timeout, which needs Python 3.11 or newer, is there because a hung connection makes the endpoint hang rather than fail. Without it the monitor times out instead of reading your 503, and you lose the reason from the response body.
The Docker side
If the app runs in a container, point the runtime at the same URL so the container marks itself unhealthy on the signal your monitor alerts on. Docker by itself will not restart an unhealthy container, it only reports the state, and compose uses it for depends_on with condition: service_healthy. The -f flag makes curl exit non-zero on a 503, which is what the HEALTHCHECK contract reads. In a Dockerfile the line is HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8000/health.
services:
api:
build: .
ports:
- '8000:8000'
healthcheck:
test: ['CMD', 'curl', '-fsS', 'http://localhost:8000/health']
interval: 30s
timeout: 3s
retries: 3
start_period: 10s Slim Python base images do not ship curl. Install it in the image or swap the test for a python -c one-liner, because a HEALTHCHECK that fails on a missing binary marks the container unhealthy forever and tells you nothing about the app.
What to leave out
- Downstream HTTP calls. Checking a partner API means their incident becomes your alert and your container restart loop.
- Anything slow. Two seconds is a generous ceiling here, and the Logdash pinger abandons the request at 10 seconds and records it as down, so a slow endpoint is indistinguishable from a broken one.
- The version string, the git SHA, the settings object. This route is public. Return a status and, at most, the name of the dependency that failed.
- Migrations, disk usage and anything else that only matters at boot. Check it in a startup hook and let the process refuse to start instead.
Point a monitor at it
- 1 Deploy and verify the failure path Stop the database container while the API keeps running, then hit the route. A 200 here means the endpoint is lying to you and no monitor can fix that.
- 2 Add the HTTP monitor Create a Logdash service, paste the public URL and let the first check run immediately. Free checks every 5 minutes, Builder every minute, Pro every 15 seconds, with the status code and response time stored each time.
- 3 Watch the alert land Kill the database once more and wait a single interval. The monitor transitions to down and Telegram delivers the endpoint name and the 503, which is the confirmation that the wiring works end to end.
Liveness and readiness are not one route
If Kubernetes is in the picture, split them. The liveness probe should touch nothing, because a database blip that answers 503 will restart every healthy pod you have and turn a two-minute outage into a crash loop. The readiness probe is the one that runs SELECT 1. Point your external monitor at the readiness route: you want the version that fails when customers cannot be served, not the version that only fails when Python has stopped.
What is a good FastAPI health check example?
An async route on /health that runs one SELECT 1 through the session inside asyncio.timeout, returns {"status": "ok"} on success, and returns JSONResponse with status_code=503 on any exception. That is the whole thing, and anything longer is usually checking too much.
How do I add a Docker health check for FastAPI?
Add HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8000/health to the Dockerfile, and install curl in the image. The -f flag turns a 503 into a non-zero exit, which is how Docker marks the container unhealthy.
How do I set a health check in docker compose for FastAPI?
Put a healthcheck block on the service with test, interval, timeout, retries and start_period. Give it a start_period long enough to cover Uvicorn boot and any migration step, otherwise the container is marked unhealthy before it has had a chance to come up.
What status code should the health route return?
200 when the app can serve a request and 503 when it cannot. Avoid 500 for a dependency failure: 503 says temporarily unavailable, which is the truth, and it is the code load balancers and orchestrators expect on this route.
Should the health route be in the OpenAPI schema?
Pass include_in_schema=False on the decorator if it clutters your docs. It changes nothing about the response, and a monitor never reads the schema.