Django health check endpoint

Write a view that runs SELECT 1 on the default connection and returns JsonResponse with status=503 when it raises, wire it into urls.py, or install django-health-check if you want the batteries-included version.

Gunicorn does not notice when Postgres goes away. The workers stay up, the socket stays open, and Django keeps routing requests to views that now raise OperationalError on the first query. A health view that returns an empty 200 sails through all of it, because it never asks the database anything. You end up with a monitor reporting perfect uptime while your error tracker fills up, which is worse than having no monitor, since a green dashboard is an argument against looking.

The fix is one query and one honest status code. Run something cheap through the connection you already have, and return 503 when it fails. Logdash counts anything outside 200-399 as down and sends the alert on the flip, so the 503 is the piece that converts a URL into a page at 3am. Without it the endpoint is a very fast way of confirming that Python has not segfaulted.

The view

health/views.py
from django.db import connection
from django.http import JsonResponse


def health(request):
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
    except Exception:
        # 503: the process is alive, the app cannot serve a request
        return JsonResponse({"status": "down", "database": "unreachable"}, status=503)

    return JsonResponse({"status": "ok"})
config/urls.py
from django.urls import path

from health.views import health

urlpatterns = [
    path("health/", health, name="health"),
]

Keep the view out of any middleware that needs a session or a logged-in user, and do not decorate it with login_required. A monitor arrives with no cookies and no auth header, so a redirect to the login page turns into a 302, which lands inside 200-399 and reads as healthy while the real app is broken.

The package version

django-health-check on PyPI is the batteries-included option and a reasonable choice. Version 4 wants Django 5.2 or newer and installs extras per backend: pip install "django-health-check[celery,redis]". Add health_check to INSTALLED_APPS, then wire HealthCheckView into urls.py with an explicit checks list naming entries like health_check.Database, health_check.contrib.redis.Redis and health_check.contrib.celery.Ping. You get a rendered status page and a non-200 when a check fails. The trade is a dependency plus a checks list that is easy to over-fill.

A web check says nothing about your workers

This is the gap most Django setups have. The web tier and the Celery workers fail independently. The broker can be reachable from gunicorn while every worker container is dead, and your /health endpoint will not notice, because nothing in the request path touches them. Jobs pile up in the queue, emails stop going out, and the monitor stays green for as long as it takes somebody to complain.

health/views.py
from django.http import JsonResponse

from config.celery import app as celery_app


def worker_health(request):
    replies = celery_app.control.ping(timeout=1.0)

    if not replies:
        return JsonResponse({"status": "down", "workers": 0}, status=503)

    return JsonResponse({"status": "ok", "workers": len(replies)})

control.ping broadcasts to every worker and collects replies until the timeout, so keep it to a second and serve it on a second URL rather than folding it into the main one. Logdash also has push heartbeats, where the worker calls out instead of being polled, but they are Pro-only and expect a ping every check interval, which makes them right for a worker that runs continuously and wrong for a nightly job.

Point a monitor at it

  1. 1
    Test both failure modes Stop Postgres and curl the health URL, then bring it back, stop the workers and curl the worker URL. Two 503s means both endpoints are telling the truth.
  2. 2
    Create the monitors One Logdash project per endpoint, since a project carries one monitor. The free plan covers five projects at a 5-minute interval, Builder drops it to a minute and Pro to 15 seconds.
  3. 3
    Confirm the alert Scale the workers to zero and leave it for one interval. The worker monitor turns red and the Telegram message arrives with the URL and the 503, so you find out from your phone rather than from a queue with 40,000 items in it.

ALLOWED_HOSTS will reject the monitor

Django validates the Host header before your view runs. A request whose Host is not in ALLOWED_HOSTS gets a 400 and never reaches the health check, which reads as down and produces an alert that has nothing to do with your database. That is fine when the monitor hits your real domain, and a constant false alarm when a load balancer probes the pod by IP. Add the host to the list.

Is django-health-check on PyPI worth installing?

Yes if you want database, cache, storage and Celery checks without writing them. Install it with the extras you need, add health_check to INSTALLED_APPS and register HealthCheckView with an explicit checks list. A hand-written view is fifteen lines and no dependency, so both answers are defensible.

How do I health check Celery workers in Django?

Call celery_app.control.ping(timeout=1.0) from a view and return 503 when the reply list is empty. Serve it on its own URL: a web health check tells you nothing about whether any worker is consuming the queue.

Should the health check be middleware instead of a view?

A view is simpler and easier to reason about. Middleware is only worth it when you need the check to answer before other middleware runs, for example when an auth or tenant middleware would otherwise reject a monitor that has no session.

What about the Docker health check for Django?

Add HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8000/health/ to the Dockerfile and make sure the container hostname is in ALLOWED_HOSTS, otherwise every probe returns 400 and the container is permanently unhealthy.

Does the endpoint need to check the database?

If the app cannot serve a page without it, yes. One SELECT 1 through the existing connection costs almost nothing and is the difference between a monitor that detects an outage and a monitor that confirms Python is running.

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

Any public URL · checked every 15 s