Monitoring Celery beat periodic tasks
Celery beat is how most Django and Python projects run periodic tasks: nightly reports, cleanups, syncs with external APIs. It is also easy to lose without noticing. Beat is a single process that only sends tasks to the broker; workers run them. If beat stops, nothing fails and nothing is logged as an error. The tasks simply stop being sent.
What goes wrong
- Beat isn’t running. It wasn’t added to the new deployment, its container exited, or a supervisor config lost the
beatline. Workers stay healthy and idle. - Two beats are running. Beat must run as exactly one instance. Run it in every replica of a container and each task goes out several times.
- Tasks fail in workers. The exception lands in the worker log. Unless you collect and read those logs, nobody sees it.
- Tasks pile up. Workers are busy or down, the queue grows, and the 3 AM task runs at noon.
- Time zone confusion. With
enable_utcand notimezoneset,crontab(hour=3)means 3:00 UTC.
A small ping helper
Put this decorator next to your tasks. It reports the start, the success or the error of every run. A failing ping never breaks the task:
# monitoring.py import functools import urllib.request from celery.exceptions import Retry def _ping(url, body=None): try: data = body.encode()[:100_000] if body else None urllib.request.urlopen(url, data=data, timeout=10) except Exception: pass # monitoring must never break the task def heartcron(url): def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): _ping(url + "/start") try: result = fn(*args, **kwargs) except Retry: raise # a retry isn't a failure yet except Exception as e: _ping(url + "/fail", f"{type(e).__name__}: {e}") raise _ping(url) return result return wrapper return decorator
Wrap the tasks
Put @app.task on the outside, so Celery registers the wrapped function:
# tasks.py
from .celery import app
from .monitoring import heartcron
@app.task
@heartcron("https://ping.heartcron.com/<uuid>")
def nightly_report():
build_and_send_report()
And the schedule, with the time zone set explicitly:
# celery.py
from celery.schedules import crontab
app.conf.timezone = "America/New_York"
app.conf.beat_schedule = {
"nightly-report": {
"task": "app.tasks.nightly_report",
"schedule": crontab(hour=3, minute=0),
},
}
In Heartcron, create a check with the cron expression 0 3 * * *, the same time zone, and a grace period a little longer than the task usually takes. Not sure how a Celery crontab() maps to cron? Its arguments go in the order minute, hour, day of week, day of month, month of year, while the cron fields are minute, hour, day of month, month, day of week. The cron expression explainer helps check the result.
What each outcome looks like
| Situation | What Heartcron sees |
|---|---|
| The task runs and succeeds | /start, then success with the duration |
| The task raises an exception | /fail with the error text, alert right away |
| The task hangs | /start without success, alert after the grace period |
| Beat, the broker or all workers are down | Nothing arrives, alert after the grace period |
| Two beats send the task twice | Two runs in the event log where you expect one |
Watch beat itself
A task that runs once a day tells you beat is gone only a day later. Add a tiny task every five minutes whose only job is to ping. It proves that beat, the broker and at least one worker all work:
@app.task
@heartcron("https://ping.heartcron.com/<uuid>")
def heartbeat():
pass
app.conf.beat_schedule["heartbeat"] = {
"task": "app.tasks.heartbeat",
"schedule": 300.0,
}
Give its check a 5-minute period and a grace period of 5–10 minutes.
Many tasks without many URLs
A readable URL from your project’s ping key and a short name saves you from copying a UUID per task. Give each check a short name, such as sync-stripe, in the dashboard or with heartcron checks create:
@heartcron("https://ping.heartcron.com/<ping-key>/sync-stripe")
Plain cron jobs in the same project, such as Django management commands, can be wrapped without code changes: heartcron run cleanup -- python manage.py clearsessions with the open-source heartcron CLI.