GUIDE · CELERY · FREE

Monitor Celery beat tasks: task_success and task_failure signals with LastPing

Connect Celery's built-in task_success and task_failure signals to ping LastPing on success and POST to /fail on failure. Filter by sender to scope each signal to a specific task. Alternatively, use a @monitored decorator for self-contained task instrumentation. Absence detection covers a down beat process. Free, no monitor cap.

The snippets

Create a monitor in the console, set the schedule to match your beat schedule plus a grace window, and copy the ping URL.

Signals — per-task scope with sender=

Connect the signals in your tasks module after defining the task. Filtering by sender ensures only the monitored task triggers the ping:

import requests from celery import Celery from celery.signals import task_success, task_failure app = Celery("tasks", broker="redis://localhost:6379/0") PING = "https://ping.lastping.dev/<your-monitor-id>" @app.task(bind=True, max_retries=3) def nightly_report(self): # your task body pass # connect after the task is defined so `sender` resolves @task_success.connect(sender=nightly_report) def on_success(sender, result, **kwargs): requests.get(PING, timeout=10) @task_failure.connect(sender=nightly_report) def on_failure(sender, task_id, exception, traceback, **kwargs): msg = f"{type(exception).__name__}: {exception}" requests.post(f"{PING}/fail", data=msg[:10_000], timeout=10) # celery beat schedule (celeryconfig.py or app.conf): app.conf.beat_schedule = { "nightly-report": { "task": "tasks.nightly_report", "schedule": 86400.0, # every 24 h in seconds }, }

Decorator approach — self-contained per task

A @monitored decorator wraps the task body directly. Cleaner when you want the ping URL co-located with the task definition rather than in separate signal handlers:

import functools, requests from celery import Celery app = Celery("tasks", broker="redis://localhost:6379/0") def monitored(ping_url): """Decorator that pings LastPing on task success or failure.""" def decorator(task_fn): @functools.wraps(task_fn) def wrapper(*args, **kwargs): try: result = task_fn(*args, **kwargs) requests.get(ping_url, timeout=10) return result except Exception as exc: msg = f"{type(exc).__name__}: {exc}" requests.post(f"{ping_url}/fail", data=msg[:10_000], timeout=10) raise return wrapper return decorator @app.task(bind=True, max_retries=3) @monitored("https://ping.lastping.dev/<your-monitor-id>") def nightly_report(self): # your task body pass

Retry guard — ping /fail only on final failure

By default, task_failure fires on each failed attempt including retries. To ping /fail only when retries are exhausted, check sender.request.retries against sender.max_retries:

@task_failure.connect(sender=nightly_report) def on_failure(sender, task_id, exception, traceback, **kwargs): task = sender # only ping /fail on the last retry if task.request.retries >= task.max_retries: msg = f"{type(exception).__name__}: {exception}" requests.post(f"{PING}/fail", data=msg[:10_000], timeout=10)

What celery beat going down looks like

If the beat process stops, no tasks are scheduled. No signal fires, no ping is sent. Absence detection is the correct safety net: the missing ping opens an incident after the grace window regardless of why the task didn't run. See also the Airflow guide for a comparison with DAG-level callbacks, the Python guide for script-level monitoring, and the LastPing MCP server for AI-agent provisioning.


What this catches

The signals report what they can; the silence reports the rest.

  • Task exceptiontask_failure fires and POSTs the exception class and message to /fail, opening an incident immediately.
  • Task successtask_success fires and GETs the success ping, resetting the absence timer.
  • celery beat is down — no task is scheduled, no signal fires; the missing ping opens the incident after the grace window.
  • Worker process killed — the in-flight task is lost; no success ping arrives; the absence opens the incident.
  • Recovery — the next successful task run fires task_success, closing the incident automatically.

Questions people ask

Front-loaded answers — the most important fact first.

  • Does task_failure fire on every retry?

    Yes, by default. To ping /fail only on final failure, check sender.request.retries >= sender.max_retries inside the handler before posting. Alternatively, use after_return on the task and check the state argument for FAILURE.

  • What if celery beat is down?

    A down beat process never enqueues tasks. No signal fires, no ping is sent. Absence-detection monitoring catches this: the missing ping after the grace window opens an incident — no signal handler code required.

  • Signals vs. decorator — which is better?

    Signals are better when you want a single place to configure monitoring for multiple tasks. The decorator is better when you want the ping URL co-located with the task and don't want global signal handlers. Both work; choose based on your team's conventions.

  • Is this free?

    Yes. LastPing is free and fully hosted with no monitor cap and no paid tier. See how it compares to Healthchecks.io and Cronitor.

FREE · FULLY HOSTED · NO PAID TIER

A down celery beat fires no signal. Absence detection does.

Two signal handlers now beat a silent task queue later. First alert wired up in about a minute — and LastPing monitors itself the same way.