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.
Your assistant can do this for you
Monitor my Celery tasks with LastPing
On this page
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 exception:
task_failurefires and POSTs the exception class and message to/fail, opening an incident immediately. - Task success:
task_successfires 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.
-
How do I monitor a Celery beat task with LastPing?
Connect the
task_successandtask_failuresignals fromcelery.signals. In the success handler GEThttps://ping.lastping.dev/<your-monitor-id>; in the failure handler POST the exception message to/fail. Use thesenderargument to filter by specific task class. -
What is the difference between signals and a decorator approach?
Signals are global: they fire for every task unless you filter by sender, and they are better when you want a single place to configure monitoring for multiple tasks. A decorator wraps specific tasks and is cleaner when only some tasks need monitoring, or when you want the ping URL co-located with the task and don't want global signal handlers. Both approaches work; signals require less boilerplate for a single-task setup. Choose based on your team's conventions.
-
What if celery beat is down and never schedules the task?
If celery beat is down, it never enqueues tasks: no task is scheduled, no signal fires, and no ping is sent. Absence-detection monitoring catches this: the missing ping after the schedule+grace window opens an incident even if the beat process never starts, with no signal handler code required.
-
Does task_failure fire on every retry?
Yes, by default:
task_failurefires on every failed attempt, including retries. To ping/failonly on the final failure, checksender.request.retries >= sender.max_retriesinside the handler before posting. Alternatively, useafter_returnon the task and check thestateargument forFAILURE. -
Is Celery task monitoring free?
Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.
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.