Celery task monitoring inside the Django admin you already run. No Flower, no extra process, no broker polling — just a reusable app that records task lifecycle events via Celery signals.
Flower is a separate service to deploy, secure and keep running. Most Django + Celery teams just want to answer three questions:
- Which tasks are failing, and with what traceback?
- Which tasks are retrying more than they should?
- Which tasks are slow (avg / p95)?
TaskPulse answers those from the Django admin, using only your existing
database. Workers write one row per task id as tasks move through
PENDING → STARTED → SUCCEEDED / FAILED / RETRIED.
pip install django-taskpulse
Or from source:
pip install git+https://github.com/dilenshah23/django-taskpulse.git
- Add the app and migrate:
# settings.py
INSTALLED_APPS = [
# ...
"django_taskpulse",
]python manage.py migrate django_taskpulse
-
That's it. The app connects to Celery's
task_received,task_prerun,task_postrun,task_failureandtask_retrysignals when Django starts, so any process that runs your Django app (including your Celery workers, assuming the standarddjango-celerysetup where workers load your settings) starts recording task runs immediately. -
Open Admin → TaskPulse → Task runs to browse runs, and the Stats button on that page for per-task aggregates.
A read-only admin changelist with:
- colored state badges (
PENDING,STARTED,SUCCEEDED,FAILED,RETRIED) - filters by state and task name, plus a date hierarchy on queue time
- search by task id or task name
- runtime, retry count, worker hostname and truncated args/kwargs per run
- full exception traceback text on failed runs
Rows are keyed by Celery task id, so a task that retries stays a single row with an incrementing retry counter instead of N duplicates.
Admin → Task runs → Stats aggregates the recent window (24h by default):
counts by state, failure rate, and avg/p95 runtime per task name, worst
failure rate first. The same numbers are available programmatically:
from django_taskpulse.stats import compute_stats
for row in compute_stats(window_hours=24):
print(row.task_name, f"{row.failure_rate_percent:.1f}%", row.p95_runtime)Task history is pruned with a management command, typically from cron or Celery beat:
python manage.py taskpulse_purge # honors TASKPULSE_RETENTION_DAYS
python manage.py taskpulse_purge --days 7 # explicit override
python manage.py taskpulse_purge --dry-run # report only
All settings are optional and read lazily:
TASKPULSE_ENABLED = True # master switch; receivers no-op when False
TASKPULSE_CAPTURE_ARGS = True # store repr() of args/kwargs
TASKPULSE_MAX_ARG_LENGTH = 512 # truncation limit for each stored repr
TASKPULSE_RETENTION_DAYS = 30 # used by `taskpulse_purge`
TASKPULSE_STATS_WINDOW_HOURS = 24 # look-back window for the stats dashboardSet TASKPULSE_CAPTURE_ARGS = False if task arguments may contain secrets or
personal data.
Celery emits signals inside the worker for every task lifecycle event.
TaskPulse's AppConfig.ready() connects five receivers that upsert a
TaskRun row keyed by task id:
task_received→ row created asPENDINGwith queue time and arg reprstask_prerun→STARTED, records start time and worker hostnametask_retry→RETRIED, increments the retry counter, stores the reasontask_failure→FAILED, stores the traceback texttask_postrun→ sets finish time, computes runtime, finalizes state
Receivers never raise: a monitoring failure is logged and the task proceeds untouched. There is no polling and no message inspection — if your worker can reach your database, TaskPulse works.
- Recording happens in the worker, so tasks queued but never picked up by a
worker won't appear (there is nothing to observe them).
task_receivedcovers the queued-but-not-yet-started window on busy workers. - One extra DB write per lifecycle event. For very high-volume tasks (hundreds/sec), either disable TaskPulse for those workers or accept the write load; there is no sampling yet.
- With
task_always_eager(tests, local dev),task_receiveddoesn't fire — rows are created attask_preruninstead.
The example/ directory contains a minimal wired-up project with
demo tasks that succeed, fail, retry and sleep:
cd example
pip install -e "..[dev]"
python manage.py migrate
python manage.py createsuperuser
python manage.py demo_seed # run demo tasks eagerly, no broker needed
python manage.py runserver
Then open http://127.0.0.1:8000/admin/django_taskpulse/taskrun/. To exercise a
real worker instead, set CELERY_BROKER_URL (e.g. Redis) and run
celery -A config worker -l info alongside.
- Python 3.10 – 3.12
- Django 4.2+ and 5.x
- Celery 5.x
Runtime dependencies are Django and Celery only.
git clone https://github.com/dilenshah23/django-taskpulse.git
cd django-taskpulse
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
pytest
ruff check .
Pre-commit hooks (ruff, ruff format, isort) are configured in
.pre-commit-config.yaml.
MIT © Dilen Shah