Chords are the most fragile canvas primitive, and their behavior under failure surprises even experienced Celery users.
A group is simple: each signature is sent as its own message carrying a shared group id, and a GroupResult holds their AsyncResults. Nothing coordinates them on the worker side.
A chord must run its callback exactly when all header tasks are done, and workers don't talk to each other, so coordination happens in the result backend:
- With Redis (and other key-value backends supporting it), each header task, on finishing, stores its result and atomically increments a counter for the chord. The task that brings the counter to the header size triggers the callback, which receives all results read back from the backend.
- Backends without that support fall back to a
celery.chord_unlocktask that polls periodically until all header results are ready, which is slower and adds load.
Consequences:
- Header tasks must store results:
ignore_result=Trueon them breaks the chord. - If any header task fails, the callback is not called; the chord fails with a
ChordError, and you needlink_erroron the chord to react. - The callback receives every result at once, so huge headers with large results mean big backend reads and memory use. Prefer returning ids or small summaries.
- Chords inside chains and nested groups are supported but harder to debug; keep workflows shallow, and test failure paths explicitly.
Example
from celery import Celery, chord, group
app = Celery("proj", broker="memory://", backend="cache+memory://")
app.conf.task_always_eager = True # demo inline
@app.task
def count_words(doc): return len(doc.split())
@app.task
def summarize(counts): return {"docs": len(counts), "words": sum(counts)}
docs = ["a b c", "d e", "f"]
g = group(count_words.s(d) for d in docs)
print(g.apply_async().get()) # [3, 2, 1]
res = chord(g)(summarize.s())
print(res.get()) # {'docs': 3, 'words': 6}
# Real backends: the last finishing header task increments the chord
# counter to 3 and triggers summarize([...all results...]).Remember
- Groups are independent messages sharing a group id
- Chords coordinate via the result backend (counter on Redis, polling elsewhere)
- Header tasks must store results; failures skip the callback
- Keep header results small: the callback reads them all
Watch out for
- Setting task_ignore_result=True globally and then using chords.