How to Monitor a Django Management Command

A Django upgrade changes how a queryset behaves, a management command that's been running fine for a year quietly starts throwing on line 40, and nobody notices because it was scheduled through cron and nobody's tailing that log. Three weeks later, someone asks why the weekly digest emails stopped going out. That gap, between when something breaks and when a human finds out, is exactly what heartbeat monitoring closes.

The idea is simple: your command pings a unique URL when it finishes successfully, and if that ping doesn't arrive on schedule, you get an alert instead of a delayed complaint. Here's how to add that to a Django management command using the Downdar API.

What You Will Need

A Downdar API key from your account, a Django project with a management command already defined, and the requests library, which is likely already a dependency in most Django projects. Your API key should live in settings via an environment variable, never committed to your repository.

Creating a Heartbeat Monitor

The /api/v1/heartbeats/create endpoint accepts a POST request with a name and returns the full heartbeat object, including the unique url your command will call on schedule. You only need to do this once per command. If you'd rather not script it, creating the heartbeat from your Downdar dashboard and copying the URL works just as well, the API call is mainly useful when you're provisioning monitoring as part of a deploy pipeline across multiple environments.

# One-off setup, run from a shell or a management command of its own. import requests   response = requests.post( 'https://downdar.com/api/v1/heartbeats/create', headers={'Authorization': 'Bearer YOUR_API_KEY'}, json={'name': 'Weekly digest emails'} )   result = response.json()

The response looks like this:

{ "ok": true, "heartbeat": { "guid": "550e8400-e29b-41d4-a716-446655440000", "name": "Weekly digest emails", "url": "https://downdar.com/heartbeats/550e8400-e29b-41d4-a716-446655440000", "paused": false }, "capacity": { "limit": 10, "used": 1, "remaining": 9 } }

Add the url to your settings as DOWNDAR_HEARTBEAT_URL, sourced from an environment variable rather than hardcoded.

Pinging the Heartbeat From Your Command

The cleanest place to add the ping is inside the command's own handle() method, so success and failure are determined by the same logic that already decides whether your command worked.

# myapp/management/commands/send_digest.py import requests from django.conf import settings from django.core.management.base import BaseCommand   class Command(BaseCommand): help = 'Sends the weekly digest email to subscribed users'   def handle(self, *args, **options): send_weekly_digest() # raises on failure, like any normal Django code   # Only reached if the line above didn't raise. try: requests.get(settings.DOWNDAR_HEARTBEAT_URL, timeout=10) except requests.RequestException: # A failed ping shouldn't fail the command itself, just log it. self.stderr.write('Heartbeat ping failed, digest still sent successfully')

The structure here matters more than it might look: send_weekly_digest() runs first and is allowed to raise normally, if it fails, Django's command machinery reports the error and the code never reaches the ping. Only a command that actually completes gets to the try block that pings the heartbeat. And the ping itself is wrapped separately, so a flaky network call to Downdar can't cause a successful digest run to report as an error.

If you're scheduling this through plain cron rather than something like Celery beat, the same effect works without touching the command file at all:

# crontab: run manage.py, then ping only if it exits 0 0 8 * * 1 cd /opt/myproject && /opt/myproject/venv/bin/python manage.py send_digest && \ curl -fsS -m 10 https://downdar.com/heartbeats/550e8400-e29b-41d4-a716-446655440000

Either approach works. Wiring the ping into handle() keeps it version-controlled alongside the command; wiring it into crontab keeps the command itself free of any monitoring-specific code. Pick whichever fits how your team already manages scheduled tasks.

Checking Heartbeat Status Programmatically

To surface a heartbeat's current state, say, in an internal Django admin view, the /api/v1/heartbeats/status endpoint returns just the essentials.

response = requests.post( 'https://downdar.com/api/v1/heartbeats/status', headers={'Authorization': f'Bearer {settings.DOWNDAR_API_KEY}'}, json={'guid': '550e8400-e29b-41d4-a716-446655440000'} )   status = response.json()['status'] # "unknown", "up", "warn", "down", or "paused"

That gives you a quick way to display "last digest run: healthy" in a place your team already looks, rather than everyone needing a separate login to Downdar to check.

A Note on API Key Security

Keep the API key and heartbeat URL out of your settings file directly, and read them from the environment instead, the same way you'd already be handling your database credentials or secret key.

# settings.py import os   DOWNDAR_API_KEY = os.environ['DOWNDAR_API_KEY'] DOWNDAR_HEARTBEAT_URL = os.environ['DOWNDAR_HEARTBEAT_URL']

The heartbeat URL itself doesn't need the same level of protection as the API key, it only accepts a check-in and can't read or modify your account, so it's fine sitting in the same environment configuration as the rest of your app. The API key is what should never end up in a settings file that gets committed, a debug page, or anything a user could reach.

Get Started

Set up a heartbeat for your most important management command first, whichever one causes the most trouble when it silently stops running. The rest of your scheduled commands are the same handful of lines away once that first one's working.