← Knowledge hub
Tutorialby Uptime Room· September 24, 2026· 6 min read

Django Health Check: Endpoint, Probes and Alerts

A django health check should verify the database, cache and broker, answer 503 when one is gone, and be watched from outside your own cluster.

A django health check is a small endpoint your application exposes so something outside it can ask whether the database, cache and broker are reachable. Django ships no such view of its own. You either install a package or write about twenty lines yourself, then point a monitor at the result and decide what a failure should mean.

What should a Django health check actually verify?

Check the dependencies a request cannot survive without, and stop there. For most projects that means the default database connection, the cache backend, and the task broker when Celery work happens on the request path. Each backend should do real work: open a cursor, write and read a key, not merely confirm that a settings dictionary exists.

Resist the urge to verify everything. An endpoint that touches six third-party APIs will fail whenever any of them has a bad minute, and the pager stops meaning anything. Blackbox monitoring describes the same trade-off from the probe side of the wire.

There is a second rule that matters more than the list itself. The endpoint has to be cheap, because it runs constantly. A load balancer may call it every five seconds per pod, so a query that scans a table will quietly become your busiest query in production. Keep every backend to a single round trip with no joins and no user data.

Installing the django-health-check package

The django-health-check package gives you pluggable backends plus a view that renders HTML or JSON. Add the base app first, then one app per subsystem you care about:

# settings.py
INSTALLED_APPS = [
    # ...
    "health_check",
    "health_check.db",
    "health_check.cache",
    "health_check.contrib.celery_ping",
]

Route it next. The convention in the project's own documentation is /ht/:

# urls.py
from django.urls import include, path

urlpatterns = [
    path("ht/", include("health_check.urls")),
]

A request to /ht/?format=json returns an object mapping each backend to a status string such as working. The view answers 500 when any backend raises. That JSON body, not the status code, is the part worth asserting on later.

Writing the endpoint by hand

A package is more machinery than a single-database service needs. Twenty lines produce the same signal with nothing to upgrade:

# views.py
from django.core.cache import cache
from django.db import connection
from django.http import JsonResponse


def healthz(request):
    result, code = {}, 200
    try:
        with connection.cursor() as cur:
            cur.execute("SELECT 1")
        result["db"] = "working"
    except Exception as exc:
        result["db"] = f"failed: {exc.__class__.__name__}"
        code = 503
    cache.set("healthz", "1", 10)
    result["cache"] = "working" if cache.get("healthz") == "1" else "failed"
    if result["cache"] != "working":
        code = 503
    return JsonResponse(result, status=code)

Answer 503 rather than 500 when a dependency is missing. Both a load balancer and an orchestrator read 503 as "temporarily out of rotation", which is exactly what you mean, and RFC 9110 says the code carries that sense.

What about Celery, storage and third-party APIs?

Celery is the one extra worth adding, and only when a request enqueues work it cannot complete itself. The celery_ping backend broadcasts a ping to workers and waits for a reply, which proves the broker is reachable and that at least one worker is alive. That second half is the value: a reachable broker with no consumers is a queue that grows forever while every request still returns 200.

Storage backends are a judgement call. If uploads are core to the product, write and delete a tiny object. If they are a side feature, leave the check out and monitor the bucket separately, because object storage fails independently of your app and on a different timescale.

Third-party APIs belong in a separate endpoint entirely. Payment providers, mail relays and search vendors each have their own status page and their own recovery time. Folding them into one boolean turns an informative signal into a coin flip, and the on-call engineer learns nothing from the alert.

Liveness, readiness and the public probe

Three callers want three different answers, so one URL rarely serves all of them well. Kubernetes restarts a container whose liveness probe fails, and it removes a pod from a Service whose readiness probe fails. An external probe cares about neither and only wants to know whether a user in Singapore can load the page.

CallerReal questionWhat it should request
Liveness probeIs this process wedged?A static 200 view with zero dependencies
Readiness probeCan this pod serve traffic now?The dependency check, 503 on failure
External uptime probeCan anyone reach the app at all?A public URL through real DNS and TLS

Pointing a liveness probe at the dependency check is the classic outage amplifier. The database hiccups, every pod fails its probe, every pod restarts, and a thirty-second blip becomes a cold-start stampede. The Kubernetes probe documentation keeps the two roles separate for that reason.

Watching the endpoint from outside

Internal probes die with the cluster they run inside. A check that starts on the public internet is the only one that survives a bad ingress rule, an expired certificate or a DNS change. Because the package returns 200 with a partially degraded body in some configurations, assert on the body text instead of the code:

curl -X POST https://uptimeroom.com/api/v1/monitors \
  -H "Authorization: Bearer urm_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "django ht",
    "type": "keyword",
    "target": "https://example.com/ht/?format=json",
    "intervalSec": 60,
    "keyword": "failed",
    "keywordMatch": "absent"
  }'

We prefer 60 seconds here. Faster intervals mostly add database round trips, since every call opens a cursor and writes a cache key. Spread the check across two of the six probe regions available as of September 2026, so one bad network path cannot page anyone on its own. Egress addresses for allowlists live on the probe IP page, and the full monitor schema is in the API reference.

Watch the timing breakdown as well as the verdict. An HTTP-family check reports DNS resolution, TCP connect, TLS handshake, time to first byte and content download as separate numbers. When a django health check starts answering slowly, that split says whether the delay sits in your view, in the certificate chain or in the resolver. You learn it before opening a log file.

Mistakes that make the endpoint lie

Running migrations checks in the probe. A pending-migrations backend turns every deploy window into an incident. Track schema drift in CI, where a human can read the failure.

Letting the URL be guessable and unauthenticated. The JSON body names your backends and sometimes your broker. Put it behind a long random path segment, or require a shared header, and keep the boring static view public for the load balancer.

Treating one failed request as an outage. A single probe failure is a blip. Confirm from a second region before the alert fires, the way cross-region quorum describes.

A good django health check is boring: two or three dependencies, a 503 when one of them is gone, and a body specific enough to assert on. Build the endpoint, split it from your liveness probe, then watch it from outside your own network. Watching it from outside costs nothing to start, since the free plan includes ten monitors at a one-minute interval as of September 2026.

FAQ

What is a health check in Django?

It is a URL your Django project exposes that runs a quick test against each dependency a request needs, usually the database, the cache and the task broker. The view returns 200 with a body naming each backend when everything answers, and an error status when one of them raises. Load balancers, orchestrators and external probes all read it.

Does Django have a built-in health check endpoint?

No. Django ships a system check framework for configuration problems at startup, run with manage.py check, but it exposes no runtime HTTP endpoint. You add one yourself, either with the django-health-check package from PyPI or with a short view that opens a database cursor and writes a cache key.

Why does ModuleNotFoundError: No module named health_check.db happen?

That error almost always means the base health_check app is missing from INSTALLED_APPS, or the package is not installed in the virtualenv that is actually running. Install django-health-check rather than any similarly named package, then list health_check before any of its subsystem apps, since the sub-apps import from the base one.

Should the Django health check URL be public?

The detailed one should not be. Its body names your backends and can leak broker details, so put it behind a long random path or a shared secret header. Keep a separate dependency-free view public for load balancers, and give your external uptime probe the authenticated URL through a header.

What status code should a failing health check return?

Use 503 rather than 500. RFC 9110 defines 503 as a temporary inability to handle the request, which is what a missing database connection means, and both load balancers and Kubernetes treat it as a signal to stop sending traffic without treating the process as permanently broken.

Related reading