Skip to main content
← All use cases

Monitor Celery Beat Tasks That Succeed and Do Nothing

Celery marks a task SUCCESS when it returns without raising. A nightly import that finds no input file, a query that returns no rows, or an API that answers with an empty page all return cleanly. Flower stays green and nobody hears about it.

Celery Beat adds a second problem. If the Beat process dies, no task is sent at all, and nothing in the worker logs says a task was missing.

Report the count with the run

Wrap the task so it sends a receipt when it ends. The body has the command, the exit code, the timestamps, and a count of what the task did.

from celery import shared_task

@shared_task(bind=True)
def reports_generate(self):
    started, exit_code, records = now(), 0, 0
    try:
        records = run_reports()
        return records
    except Exception:
        exit_code = 1
        raise
    finally:
        post_receipt(f"celery-{self.request.id}-{started}", exit_code, started, records)

post_receipt builds the body with build_receipt from outcome_checkin.py and posts it to /api/v1/ingest/cron with a short timeout. If Crontinel is unreachable, the call is swallowed and the task still finishes. The full helper is in the check-in recipes.

Set a rule that a run needs at least one processed_records. A run that exits 0 with a count of 0 then fails the rule and sends an alert.

Catch a Beat that stopped

Crontinel does not read your Beat schedule. Tell it what to expect, once:

curl -sS -X POST "https://app.crontinel.com/api/v1/ingest/schedule" \
  -H "Authorization: Bearer $CRONTINEL_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":"named_by_user","tasks":[{"command":"reports:generate","expression":"0 3 * * *","timezone":"UTC","grace_seconds":300,"counted_minimum":1}]}'

If no receipt for reports:generate arrives within five minutes of 03:00, a “cron never started” alert opens. It does not matter whether Beat died, the broker lost the message, or the worker was down.

What was tested

The task above ran eagerly through Celery against a local Crontinel app and landed as a completed run in the run history. The Beat-stopped alert was exercised with a registered schedule and a skipped run. It was not run against a Redis or RabbitMQ broker.

Start with the HTTP check-in recipes and the schedule registration guide.

See also

Start with one HTTP receipt

Five common runtimes post the same outcome body: curl, Node, Python, Sidekiq, and GitHub Actions. Laravel apps can add the Composer package for schedule, queue, and Horizon.

curl -X POST "$CRONTINEL_API_URL/api/v1/ingest/cron" \
  -H "Authorization: Bearer $CRONTINEL_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"nightly-import","status":"completed","exit_code":0,"outcomes":{"metrics":{"processed_records":0}}}'