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

  1. Use the canvas API: from celery import chain; chain(task1.s(), task2.s()).apply_async().
  2. Clear stale 'celery.chain' messages from the broker queue if upgrading.
  3. Audit third-party code that calls send_task('celery.chain') and switch it to the canvas primitive.
  4. 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

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


AI-assisted analysis of celery/celery@571efe8120 (2026-08-04). Data as JSON: /data/errors/32a1570c22645a30.json. Report an issue: GitHub.