Flask health check endpoint

Register a blueprint with a /health route that runs db.session.execute(text("SELECT 1")) and returns a 503 when it raises, so the check fails whenever the database the app depends on is gone.

The Flask health check almost everybody writes is a route that returns the string ok. It takes ten seconds to add and it proves that the WSGI worker is alive. That is a real fact, and it is rarely the fact you need. When the database host disappears, the worker is still alive. It accepts the connection, matches the route, returns your 200, and every other endpoint in the app is returning 500 at the same moment. The monitor sees the good number and says nothing.

What makes the endpoint useful is that it fails when the app has stopped being able to do its job. One query through the session you already have, and a 503 when that query raises. The 200-399 range is the whole of what a monitor treats as healthy, so stepping outside it is the only way to say something is wrong: Logdash marks the check down and fires once, on the change. That 503 is the line connecting a broken database to your phone, and without it there is nothing for an alert to hang on.

The blueprint

app/health.py
from flask import Blueprint, jsonify
from sqlalchemy import text

from app.extensions import db

health_bp = Blueprint("health", __name__)


@health_bp.get("/health")
def health():
    try:
        db.session.execute(text("SELECT 1"))
    except Exception:
        db.session.rollback()
        return jsonify(status="down", database="unreachable"), 503

    return jsonify(status="ok"), 200

Register it in the app factory with app.register_blueprint(health_bp). The text() wrapper is not optional on SQLAlchemy 2.0, which refuses to execute a raw string and raises ObjectNotExecutableError instead. The rollback matters too: a failed statement leaves the session in a broken state, and without it the next request handled by that worker inherits the mess.

What to check and what to leave out

  • One query against the primary database. That is the dependency Flask cannot fake its way around, and SELECT 1 through the existing pool costs a fraction of a millisecond.
  • Redis, only when a cache miss is fatal rather than slow. Most Flask apps survive a cold cache and should not go down for one.
  • No calls to other services you happen to talk to. Their downtime becomes your alert, and you spend the incident explaining that your app was fine.
  • Finish well under a second. Logdash abandons a request after 10 seconds and files the check as down, so a health route that queues behind slow traffic will alert on its own latency.
  • Keep the body to a status and the name of the failed dependency. No config values, no library versions, nothing that helps somebody who is not you.

Check what runs before the view as well. A before_request hook that requires an API key, or a login_required decorator applied to the whole blueprint, will bounce the monitor with a 401 or a redirect. A 401 reads as down and alerts constantly; a 302 reads as up and hides real outages. Leave this one route open.

Point a monitor at it

  1. 1
    Prove it fails Stop the database and call the route. If you do not see a 503 with the body you wrote, fix that before going any further, because everything downstream depends on this one status code.
  2. 2
    Create the monitor Add a Logdash service pointed at https://yourapp.com/health. Every check stores the status code and the response time, at 5 minute intervals on the free plan, 1 minute on Builder and 15 seconds on Pro.
  3. 3
    Watch it fire Take the database down one more time and wait an interval. The monitor flips to down and the Telegram alert arrives with the URL and the status code, which is the only real proof that the chain from query to notification is intact.

One worker is not the app

A single check hits one Gunicorn worker on one instance. If you run four instances behind a load balancer, a green result means at least one of them answered, not that all four are healthy. That is usually acceptable, since the load balancer should be pulling the broken one out with its own probe. Where it stops being acceptable is a rolling deploy that half fails: two instances serving the new code, two crash-looping, and an external monitor happily reporting up. Response times are recorded on every check and charted, so a jump there is often the first hint, but nothing alerts on latency by itself. The alert comes from status codes, which is another reason the 503 has to be right.

What is the simplest Flask health check example?

A blueprint with one route that runs db.session.execute(text("SELECT 1")), returns jsonify(status="ok") with a 200, and returns a 503 from the except branch. Twelve lines including imports.

What path should the Flask health endpoint use?

/health. It is the default most orchestrators, load balancers and monitoring tools reach for, and matching the convention means one less thing to configure. /healthz is the Kubernetes flavoured alternative if your cluster already standardises on it.

Should a Flask health check query the database?

Yes, if the app cannot serve a request without one. The point of the endpoint is to fail when the app is unusable, and the database is the dependency that most often makes it unusable while the process stays up.

What status code should Flask return when a check fails?

503 Service Unavailable. It says the condition is temporary, it is what load balancers act on, and a monitor treats it as down because it sits outside the 200-399 range.

Point it at your own URL and watch it for real.

Any public URL · checked every 15 s