Knowing the wire format lets you debug stuck messages, evolve task signatures safely and interoperate with other producers.
Celery sends tasks through Kombu, its messaging library, which abstracts transports (AMQP, Redis, SQS). A task message (protocol version 2, the default since Celery 4) has:
- Headers:
task(the name),id,root_idandparent_id(workflow lineage),group,retries,eta,expires,timelimit,argsrepr/kwargsrepr(safe string forms for logs and monitoring),origin. - Body: a serialized tuple
(args, kwargs, embed), whereembedholdscallbacks,errbacks,chainandchordfor canvas workflows. - Properties:
correlation_id(the task id),reply_to, content type and encoding.
The worker's consumer receives the message, looks up the task name in its registry, deserializes the body (only accepting accept_content types), and hands the request to the pool. An unknown name produces a "Received unregistered task" error and the message is rejected.
Practical consequences:
- Renaming a task or module breaks queued messages; deploy new code alongside old names or use explicit
name=strings. - Changing a task's signature must stay compatible with messages already queued: add parameters with defaults and keep accepting old ones during a rollout.
- Non-Python producers can enqueue Celery tasks by writing this format, and
app.send_task("name", args=...)lets Python clients send by name without importing the task code.
Example
from celery import Celery
app = Celery("proj", broker="memory://")
@app.task(name="billing.charge")
def charge(order_id, amount, currency="EUR"):
return f"{order_id}:{amount}{currency}"
# Build the message the client would publish, without sending it
sig = charge.s(42, 9.99)
msg = app.amqp.create_task_message(
"a1b2", "billing.charge", sig.args, sig.kwargs)
print(msg.headers["task"], msg.headers["id"], msg.headers["argsrepr"])
# billing.charge a1b2 (42, 9.99)
args, kwargs, embed = msg.body
print(args, kwargs, sorted(embed))
# (42, 9.99) {} ['callbacks', 'chain', 'chord', 'errbacks']
# Send by name, without importing the task's code:
# app.send_task("billing.charge", args=(42, 9.99))Remember
- Protocol v2: routing info in headers, (args, kwargs, embed) in the body
- Workers dispatch by task name from their registry
- Renames and signature changes must stay compatible with queued messages
- send_task() enqueues by name without importing the task
Watch out for
- Renaming a task module in a deploy while thousands of messages with the old name are still queued.