celery/celery · error · NotImplementedError
chain is not a real task
Error message
chain is not a real task
What it means
Raised by the backwards-compatibility 'celery.chain' task registered in celery/app/builtins.py. Historically a task named 'celery.chain' existed in the registry; the modern chain is a canvas primitive (celery.chain), not an executable task. The stub task raises NotImplementedError if someone sends a task message addressed to the name 'celery.chain' (e.g. via send_task('celery.chain')).
Source
Thrown at celery/app/builtins.py:161
# any partial args are added to all tasks in the group
taskit = (maybe_signature(task, app=app).clone(partial_args)
for i, task in enumerate(tasks))
with app.producer_or_acquire() as producer:
[stask.apply_async(group_id=group_id, producer=producer,
add_to_parent=False) for stask in taskit]
parent = app.current_worker_task
if add_to_parent and parent:
parent.add_trail(result)
return result
return group
@connect_on_app_finalize
def add_chain_task(app):
"""No longer used, but here for backwards compatibility."""
@app.task(name='celery.chain', shared=False, lazy=False)
def chain(*args, **kwargs):
raise NotImplementedError('chain is not a real task')
return chain
@connect_on_app_finalize
def add_chord_task(app):
"""No longer used, but here for backwards compatibility."""
from celery import chord as _chord
from celery import group
from celery.canvas import maybe_signature
@app.task(name='celery.chord', bind=True, ignore_result=False,
shared=False, lazy=False)
def chord(self, header, body, partial_args=(), interval=None,
countdown=1, max_retries=None, eager=False, **kwargs):
app = self.app
# - convert back to group if serialized
tasks = header.tasks if isinstance(header, group) else header
header = group([View on GitHub (pinned to 571efe8120)
Solutions
- Use the canvas API: from celery import chain; chain(task1.s(), task2.s()).apply_async().
- Clear stale 'celery.chain' messages from the broker queue if upgrading.
- Audit third-party code that calls send_task('celery.chain') and switch it to the canvas primitive.
- Do not reference the built-in 'celery.chain' name in routing or task_routes.
Example fix
# before
app.send_task('celery.chain', args=[...])
# after
from celery import chain
chain(step1.s(2), step2.s()).apply_async() Defensive patterns
Strategy: validation
Validate before calling
from celery import chain
# never call send_task on the legacy name
BANNED_NAMES = {'celery.chain', 'celery.chord'}
def safe_send(app, name, *a, **k):
if name in BANNED_NAMES:
raise ValueError(f'{name} is a canvas primitive, not a dispatchable task; use celery.{name.split(".")[1]}')
return app.send_task(name, args=a, kwargs=k) Type guard
def is_legacy_builtin_task_name(name: str) -> bool:
return name in ('celery.chain',) Prevention
- Use the canvas API (celery.chain, celery.group, celery.chord) instead of send_task for composition.
- Purge stale broker queues referencing 'celery.chain' after upgrades.
- Lint code for send_task calls using built-in canvas names.
When it happens
Trigger: Calling app.send_task('celery.chain', ...) or app.send_task('celery.chord') against a legacy task name; serializing/deserializing a chain and mistakenly dispatching it as a task; old client code or a stale queue message referencing the 'celery.chain' task name.
Common situations: Upgrading from a very old Celery version where these were real tasks; leftover messages in the broker queue referencing 'celery.chain'; third-party tooling that dispatches by task name without using the canvas API.
Related errors
- Tasks must define the run method.
- Cannot replace with an empty chain
- backend does not implement forget.
- Backend does not support add_to_chord
- Invalid queue type '{self.create_missing_queue_type}'. Valid
AI-assisted analysis of celery/celery@571efe8120 (2026-08-04).
Data as JSON: /data/errors/32a1570c22645a30.json.
Report an issue: GitHub.