Celery
Asynchronous
Task Chaining
Python
Distributed Systems

Celery - Asynchronous Task Chaining

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Celery task chaining lets you express a workflow where the output of one task becomes the input of the next. This is useful when tasks must run in sequence but you still want the whole pipeline to execute asynchronously in the worker system. The core tool is chain, built from task signatures such as .s() and .si().

How chain Passes Results Forward

In a chain, Celery takes the return value of one task and passes it as the first argument to the next task.

python
1from celery import Celery, chain
2
3app = Celery("demo", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0")
4
5@app.task
6def add(x, y):
7    return x + y
8
9@app.task
10def multiply(value, factor):
11    return value * factor
12
13workflow = chain(add.s(2, 3), multiply.s(10))
14result = workflow.delay()
15print(result.get(timeout=10))

Here add returns 5, and Celery calls multiply(5, 10), producing 50.

That automatic result passing is the defining behavior of a chain.

Use Signatures Deliberately

A signature is a serializable description of a task call. Celery commonly uses:

  • '.s(...) for a normal signature that accepts the previous task result'
  • '.si(...) for an immutable signature that ignores the previous result'

This difference matters when a later task should run next in the workflow but should not receive the prior output.

python
1@app.task
2def log_done(message):
3    print(message)
4    return message
5
6workflow = chain(add.s(2, 3), log_done.si("completed"))

In this example, log_done receives only "completed", not the numeric result of add.

Keep Task Interfaces Small and Stable

Chains work best when tasks return compact, serializable values. Passing huge objects through a result backend makes workflows slower and harder to debug.

A good pattern is to pass identifiers rather than large payloads.

python
1@app.task
2def create_report(user_id):
3    report_id = f"report-{user_id}"
4    return report_id
5
6@app.task
7def email_report(report_id):
8    print(f"Emailing {report_id}")

That keeps the workflow simple and makes retries safer.

Understand Failure Behavior

If a task in a chain fails, later tasks do not run. That is usually what you want, but it means each task should have clear retry and error semantics.

python
@app.task(bind=True, autoretry_for=(ConnectionError,), retry_backoff=True, max_retries=3)
def fetch_remote_data(self, record_id):
    raise ConnectionError("temporary failure")

Retries apply at the task level. If the task still fails permanently, the chain stops there.

For more complex branching or recovery, Celery primitives such as group, chord, or explicit error callbacks may be a better fit than a plain linear chain.

Build the Workflow That Matches the Dependency Graph

Use a chain when tasks truly depend on previous results in sequence. Do not use a chain when tasks are actually independent. In that case, a group is the more accurate primitive.

A healthy Celery design chooses the primitive that matches the dependency graph:

  • 'chain for sequential dependency'
  • 'group for parallel independent work'
  • 'chord for parallel work followed by aggregation'

Misusing chains for unrelated tasks usually creates unnecessary latency.

Common Pitfalls

  • Forgetting that .s() passes the previous task result into the next task automatically.
  • Passing large data blobs through the chain instead of stable IDs or small values.
  • Using chain when tasks are independent and could run in parallel.
  • Ignoring failure and retry behavior, which causes later tasks to vanish when an upstream step errors.
  • Forgetting to use .si() when a downstream task should ignore the previous result.

Summary

  • Celery chain creates an asynchronous sequence where each task can consume the previous task's result.
  • '.s() passes the prior result forward, while .si() creates an immutable signature that does not.'
  • Keep task inputs and outputs small, serializable, and stable.
  • Use retries and error handling deliberately because a failed task stops the rest of the chain.
  • Choose chain only when the workflow is truly sequential; use other Celery primitives for parallel or branching work.

Course illustration
Course illustration

All Rights Reserved.