Skip to content
Get started

How to monitor a Django management command run on a schedule

Django has no built-in scheduler, so a management command run on a schedule is driven by an external trigger — a UNIX crontab, a Celery beat task, or a platform cron. Monitoring works by having the command signal success to an external monitor as its final action. The command's handle() method should exit non-zero on failure (raise CommandError or return self.stderr.write with exit_code=1), and you ping a heartbeat monitor only when handle() returns cleanly.

How should a Django management command signal failure?

A management command that calls sys.exit(1) or raises a SystemExit causes manage.py to exit non-zero. Use CommandError for expected failure cases — Django converts it to stderr output and a non-zero exit:

management/commands/send_reports.py
from django.core.management.base import BaseCommand, CommandError
import urllib.request

PING_URL = "https://ping.cronshield.com/<your-check-id>"

class Command(BaseCommand):
    help = "Send nightly reports"

    def handle(self, *args, **options):
        try:
            send_all_reports()
        except Exception as exc:
            raise CommandError(f"report generation failed: {exc}") from exc

        # Only reached if handle() returns cleanly — no exception.
        urllib.request.urlopen(PING_URL, timeout=10)
        self.stdout.write(self.style.SUCCESS("Reports sent"))

With this pattern, a CommandError skips the ping and (if called from cron) produces output that triggers MAILTO. The heartbeat covers the silent-miss case where the cron entry was removed or the server was down.

How do I schedule and monitor it with cron?

crontab -e
# Run the command daily at 03:00. The && ensures the ping fires only on exit 0.
0 3 * * * /path/to/venv/bin/python /path/to/manage.py send_reports && curl -fsS -m 10 "https://ping.cronshield.com/<your-check-id>"
The heartbeat inside handle() and the one on the crontab line serve the same purpose — pick one. The in-command ping gives you more granular control; the crontab && is simpler for one-liners. PING_URL is a placeholder for the endpoint you get on a monitor.

What if I'm using Celery beat instead of cron?

If the management command is triggered by a Celery task (using call_command inside a Celery task), monitor the Celery beat scheduler as well. A dead beat process means the task is never dispatched, so the management command never runs — and the heartbeat monitor catches the silence.

Add a missed-run alert to this job

The free tier gives you a heartbeat endpoint and an email alert when an expected ping doesn't arrive. Paid tiers add the log-aware diagnosis — the last log line and a likely cause in the alert. The heartbeat receiver ships in an upcoming release; see the plans to learn what each tier adds.

Frequently asked questions

Does Django log management command failures automatically?
Django writes a CommandError to stderr and exits non-zero, but it does not push that to any external alerting system. If you run the command from cron with MAILTO set, the stderr output will be emailed. Without MAILTO or a heartbeat, the failure is silent.
How do I test that the heartbeat fires correctly from manage.py?
Run the command manually and confirm the ping reaches your monitor. Then introduce a deliberate CommandError and confirm no ping fires and the monitor would have alerted if the grace period passed.