Opinionated health checks for Django applications
Project description
Check the health of your Django app
This project is an opinionated library focused on Tinuvi's needs. It has been evolved from version 3.20.8 of the original library.
Installation
Install the package from PyPI:
pip install django-health-check-full-of-juice
Or, with Poetry:
poetry add django-health-check-full-of-juice
Requires Python ≥ 3.12 and Django ≥ 5.2.11.
Quick start
-
Add
health_checktoINSTALLED_APPS. No contrib app needs to be installed — backends are referenced by dotted import path inHEALTH_CHECK["SUBSETS"]and resolved on demand.INSTALLED_APPS = [ # ... "health_check", ]
-
Wire up the URLs (typically under an unauthenticated path so probes can reach it):
# urls.py from django.urls import include, path urlpatterns = [ # ... path("ht/", include("health_check.urls")), ]
-
Declare at least one named subset under
HEALTH_CHECK["SUBSETS"]. Each entry is the dotted import path to aBaseHealthCheckBackendsubclass:# settings.py HEALTH_CHECK = { "SUBSETS": { "readiness": [ "health_check.contrib.db_heartbeat.backends.DatabaseHeartbeatCheck", "health_check.contrib.migrations.backends.MigrationsHealthCheck", ], }, }
-
Hit
GET /ht/<subset-name>/from your load balancer / Kubernetes probe / monitoring tool. Returns200 OKif every backend reports healthy,500otherwise. Pass?format=jsonfor a JSON response.
There is no "run every backend" endpoint — every probe targets an explicit subset, so the response is always reproducible.
Running checks from the CLI
A management command is also available — useful in deploy pipelines and one-off diagnostics. --subset is required:
python manage.py health_check --subset readiness
The command exits non-zero if any check fails.
Recommended setup
We use a three-probe scheme in our projects. Each probe answers a different question, so each one is wired up differently.
| Route | Question it answers | How it's served |
|---|---|---|
/healthcheck/liveness |
Is the Python process alive and the request loop responsive? | LivenessMiddleware short-circuits before app code runs |
/healthcheck/readiness |
Are the crucial services this app needs to run reachable? | health_check URL include + a readiness subset |
/healthcheck/integrations |
Are all services this app depends on reachable (incl. above)? | health_check URL include + an integrations subset |
Liveness — bypass middleware and integrations
Liveness must not be gated on Django middleware, the auth stack, or any integration: if it is, a sick downstream dep can trigger a needless restart by your orchestrator. Use LivenessMiddleware and put it at the very top of MIDDLEWARE:
MIDDLEWARE = [
"health_check.middleware.LivenessMiddleware",
"django.middleware.security.SecurityMiddleware",
# ... the rest of your middleware
]
It responds to GET /healthcheck/liveness (and /healthcheck/liveness/) with 200 {"status": "ok"} and short-circuits before any other middleware runs. The path is configurable via HEALTH_CHECK["LIVENESS_PATH"] (default: /healthcheck/liveness).
If you forget to put it at index 0, python manage.py check emits warning health_check.W001 so CI can catch the mistake. To bypass the warning on a per-command basis (e.g. when running migrate in a deploy step where the ordering is intentionally different), pass Django's built-in --skip-checks flag:
python manage.py migrate --skip-checks
Readiness and integrations — two subsets
Wire the URLs and define two subsets — readiness for crucial services only, integrations for everything (readiness plus the rest):
# urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("healthcheck/", include("health_check.urls")),
]
# settings.py
HEALTH_CHECK = {
"SUBSETS": {
# Crucial services the app literally cannot serve traffic without.
"readiness": [
"health_check.contrib.migrations.backends.MigrationsHealthCheck",
"health_check.contrib.db_heartbeat.backends.DatabaseHeartbeatCheck",
],
# Everything readiness has, plus every other integration the app talks to.
"integrations": [
"health_check.contrib.migrations.backends.MigrationsHealthCheck",
"health_check.contrib.db_heartbeat.backends.DatabaseHeartbeatCheck",
"health_check.cache.backends.CacheBackend",
"health_check.storage.backends.DefaultFileStorageHealthCheck",
"health_check.contrib.redis.backends.RedisHealthCheck",
"health_check.contrib.celery_ping.backends.CeleryPingHealthCheck",
"health_check.contrib.rabbitmq.backends.RabbitMQHealthCheck",
],
},
}
Keep readiness to backends the app literally cannot function without — failing it pulls every pod out of the load balancer at once, so a non-critical hiccup (e.g. Redis) shouldn't be in there or you've turned a degraded-performance event into a full outage. CacheBackend lives in integrations only, on the assumption that sessions are stored in the database (Django's default, django.contrib.sessions.backends.db). If your project sets SESSION_ENGINE = "django.contrib.sessions.backends.cache" (or cached_db), users can't authenticate without the cache — move CacheBackend back into readiness for that project.
If your project uses Django-Q instead of (or alongside) Celery, swap in or add health_check.contrib.django_q.backends.DjangoQClusterHealthCheck to integrations. It reads the heartbeat the Django-Q sentinel publishes to its broker on every cycle, so the web tier can fail synthetic monitoring when the worker fleet stops broadcasting.
That gives you:
GET /healthcheck/liveness— orchestrator liveness probe (handled by middleware).GET /healthcheck/readiness— orchestrator readiness probe (subset).GET /healthcheck/integrations— full dependency check, e.g. for synthetic monitoring.
python manage.py check validates every dotted path in HEALTH_CHECK["SUBSETS"] at boot — typos and non-BaseHealthCheckBackend references surface as health_check.E002/E003/E004 errors so CI catches them before deploy.
Kubernetes probes
For a web application (anything that exposes HTTP), use httpGet probes:
startupProbe:
httpGet:
path: /healthcheck/readiness
port: 8080
readinessProbe:
httpGet:
path: /healthcheck/readiness
port: 8080
livenessProbe:
httpGet:
path: /healthcheck/liveness
port: 8080
Liveness hits the middleware-served path so it can't be gated on the Django stack; startup and readiness both hit the readiness subset because "ready to receive traffic" and "ready to be considered started" answer the same question for a typical Django web app.
For a consumer (Celery worker, Django-Q worker, or any process that doesn't bind a port), there's no HTTP listener to probe, so use exec probes that run the management command. Define a third subset, liveness, that contains only a backend safe to evaluate from inside the worker pod — see the per-framework recipes below — and wire all three probes:
startupProbe:
exec:
command:
- /bin/sh
- -c
- python manage.py health_check -s readiness --skip-checks
readinessProbe:
exec:
command:
- /bin/sh
- -c
- python manage.py health_check -s readiness --skip-checks
livenessProbe:
exec:
command:
- /bin/sh
- -c
- python manage.py health_check -s liveness --skip-checks
--skip-checks is load-bearing here. Consumers reuse the same Django settings module as the web app — including LivenessMiddleware at the top of MIDDLEWARE — so without --skip-checks, every probe invocation runs Django's full system-check phase before the health check itself, slowing down each probe and printing the health_check.W001 warning to stderr.
Django-Q workers
Django-Q's sentinel publishes a Stat snapshot to the broker every cycle (TTL ≈ 3 s) — so the entry vanishes within seconds when the sentinel wedges or dies. Two backends sit on top of that signal:
health_check.contrib.django_q.backends.DjangoQLocalHealthCheck— passes only if a stat keyed to the current host is fresh. Use this for the worker pod'slivenessProbe.health_check.contrib.django_q.backends.DjangoQClusterHealthCheck— passes if any stat for the configured cluster name is fresh. Use this in the web tier'sintegrationssubset.
HEALTH_CHECK = {
"SUBSETS": {
"liveness": ["health_check.contrib.django_q.backends.DjangoQLocalHealthCheck"],
# readiness / integrations as above
},
}
Tradeoff: this probe touches the Django-Q broker. With the ORM broker, a database outage will fail liveness — acceptable, since the worker can't process anything without it. With the Redis broker, a Redis outage will trigger a worker-restart storm; weigh that before adopting this for Redis-backed setups.
By default the cluster name comes from Q_CLUSTER["name"] and the unhealthy status set is {"Stopping", "Stopped"}. Override with HEALTH_CHECK["DJANGO_Q_CLUSTER_NAME"] and HEALTH_CHECK["DJANGO_Q_UNHEALTHY_STATUSES"].
Celery workers
Celery doesn't broadcast an equivalent native heartbeat, so health_check.contrib.celery_heartbeat ships a Celery worker bootstep that touches a heartbeat file on a timer; the matching backend asserts the file's mtime is fresh. Broker-independent, pod-local — exactly what a livenessProbe should be.
Install the bootstep in your Celery app definition:
from celery import Celery
from health_check.contrib.celery_heartbeat.bootsteps import LivenessProbe
app = Celery("myproject")
app.steps["worker"].add(LivenessProbe)
Then point the liveness subset at the matching backend:
HEALTH_CHECK = {
"SUBSETS": {
"liveness": ["health_check.contrib.celery_heartbeat.backends.CeleryHeartbeatHealthCheck"],
# readiness / integrations as above
},
}
Defaults: heartbeat file /tmp/celery_worker_heartbeat, refresh interval 1.0 s, max age 60 s. Override with HEALTH_CHECK["CELERY_HEARTBEAT_FILE"], HEALTH_CHECK["CELERY_HEARTBEAT_INTERVAL"], and HEALTH_CHECK["CELERY_HEARTBEAT_MAX_AGE"]. Note that CeleryPingHealthCheck (under integrations) and CeleryHeartbeatHealthCheck answer different questions — keep celery_ping where it is for synthetic monitoring; use celery_heartbeat only for the worker's own liveness.
Configuration
All configuration lives under the HEALTH_CHECK dict in your Django settings. Every key is optional and falls back to the default shown below.
HEALTH_CHECK = {
"DISK_USAGE_MAX": 90, # percent; emits a warning when disk usage is at/above this threshold
"MEMORY_MIN": 100, # MB of available RAM below which a warning is emitted
"WARNINGS_AS_ERRORS": True, # if False, ServiceWarning won't fail the endpoint with HTTP 500
"SUBSETS": {}, # named subsets — see below; required to expose any probe
"DISABLE_THREADING": False, # if True, run backends sequentially in the request thread
}
Settings are read lazily at check time via health_check.conf.get_setting, so django.test.override_settings (and any runtime mutation of settings.HEALTH_CHECK) is honored.
Subsets
Group backends so probes can target a specific slice of your stack. Each entry is one of:
- A dotted import path string —
"my.module.MyHealthCheck"— instantiated with no arguments. - A
(path, kwargs)2-element tuple/list —("my.module.MyHealthCheck", {"alias": "replica"})— instantiated asMyHealthCheck(**kwargs). Use this to spin up multiple instances of the same backend with different configuration (e.g. oneCacheBackendper cache alias). Thekwargsdict is deep-copied per request so a backend that mutates its own state can't leak into later probes.
Hit a subset via /<your-mount-point>/<subset-name>/ or python manage.py health_check --subset <subset-name>.
HEALTH_CHECK = {
"SUBSETS": {
"startup": [
"health_check.contrib.migrations.backends.MigrationsHealthCheck",
"health_check.contrib.db_heartbeat.backends.DatabaseHeartbeatCheck",
],
"integrations": [
"health_check.cache.backends.CacheBackend", # default cache
("health_check.cache.backends.CacheBackend", {"backend": "cockatiel"}), # named cache
],
},
}
If you list the same backend class more than once with different kwargs, override identifier() so each instance returns a distinct string — the response dict is keyed by identifier, and same-keyed entries silently overwrite each other. The built-in CacheBackend.identifier() already returns f"Cache backend: {self.backend}", so the example above is safe out of the box.
See Recommended setup for the readiness/integrations layout we use in production.
Writing a custom backend
Subclass BaseHealthCheckBackend, implement check_status, and reference it by dotted path in HEALTH_CHECK["SUBSETS"]:
# myapp/backends.py
from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import ServiceUnavailable
class MyServiceHealthCheck(BaseHealthCheckBackend):
critical_service = True # if False, failures don't fail the overall check
def check_status(self):
if not my_service.is_reachable():
raise ServiceUnavailable("my-service is unreachable")
# settings.py
HEALTH_CHECK = {
"SUBSETS": {
"integrations": [
# ...
"myapp.backends.MyServiceHealthCheck",
],
},
}
No AppConfig.ready() hook is needed — the backend is loaded on demand the first time the subset is hit, and python manage.py check validates the path at boot time.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_health_check_full_of_juice-0.2.0.tar.gz.
File metadata
- Download URL: django_health_check_full_of_juice-0.2.0.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.4.1 CPython/3.14.4 Linux/6.17.0-1010-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c951e7e6d581d05d2682c50d11f10a0b252d9be3be0000c047c97fbff2fa004f
|
|
| MD5 |
7db3b46249f35fa0ff05fe1c26faa2c2
|
|
| BLAKE2b-256 |
4a2998e4008a49b71c3d72035310b34ebee9ff2a0c081221ecf6d964f3b74edc
|
File details
Details for the file django_health_check_full_of_juice-0.2.0-py3-none-any.whl.
File metadata
- Download URL: django_health_check_full_of_juice-0.2.0-py3-none-any.whl
- Upload date:
- Size: 32.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.4.1 CPython/3.14.4 Linux/6.17.0-1010-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93a1dd9eea85bab33cf748de5c1423e9bb999de952d45c3623d0eccd50d28632
|
|
| MD5 |
b111ad4c46050d18677a9f6e4582c45f
|
|
| BLAKE2b-256 |
ad089021a9f61d3e2c84cdee56389edd0cd954f2917a8258c66145413164d0f3
|