Why should a Django view pass an order's primary key to a Celery task rather than the Order instance itself?
answer
- the message is serialised
- the default serializer
- a snapshot versus current data
- the row may be gone
basics
~20 sCelery serialises task arguments, JSON by default, and a Django model instance is not JSON-serialisable. Even with pickle, an instance is a stale snapshot; passing order.pk lets the task load current data and handle a deleted row.
solid answer
~40 sA Celery call is a message: `send_order_confirmation.delay(order)` must serialise its arguments, and Celery's default serializer is JSON, so a model instance fails to encode. Switching to pickle would make it work but is worse: the worker gets the order as it was when queued, possibly minutes later, and pickle messages are unsafe to accept from an untrusted broker. Passing `order.pk` keeps messages small and stable, and the task re-fetches with `Order.objects.get(pk=order_id)`, using `select_related` for what the email needs. The task must then handle `Order.DoesNotExist` if the order was deleted, and the view must queue only after the order is committed, or the worker may not find the row at all.
code
python · 9 linesfrom django.shortcuts import redirect
from .tasks import send_order_confirmation
def checkout(request):
order = request.cart.place_order()
send_order_confirmation.delay_on_commit(order.pk) # pass the id, not the instance
return redirect("orders:done", pk=order.pk)go deeper
Remember to pass order.pk to the task and load the order inside it, because task arguments are sent as a message.
Explain JSON as Celery's default serializer, which types fail, and how the task re-fetches with select_related.
Cover the snapshot, deleted-row and double-delivery cases, why pickle is a security risk, and why the id must be queued after commit.
Set task-argument conventions, ids and plain values only, as a contract that survives deploys and model changes.
## A task call is a message When a Django view calls `send_order_confirmation.delay(...)`, nothing runs in the view. Celery **serialises** the task name and arguments into a message, sends it to the **broker**, and a **worker** process — separate from the web process, often on another machine — deserialises it later and calls the function. Every argument therefore has to survive encoding, transport and decoding. ## Why an instance fails Celery's default task serializer is **JSON**. A Django model instance is not JSON-serialisable, so `delay(order)` raises an encoding error in the view. The same applies to querysets, file objects and anything else that is not plain data. | Argument | Result with the JSON serializer | |---|---| | `order.pk` | Works | | `str(order.uuid)` | Works | | `order` | Encoding error in the view | | `Order.objects.filter(...)` | Encoding error | ## Why not just switch to pickle Pickle can encode a model instance, but it trades one error for three problems: 1. **Staleness.** The worker receives a snapshot. If support edits the shipping address between queueing and execution, the confirmation email shows the old one. 2. **Deleted rows.** The pickled instance still exists after the row is deleted; the task happily emails about a cancelled order. 3. **Security.** Unpickling executes code chosen by whoever wrote the message; accepting pickle from a broker is a code-execution risk if anything else can publish to it. Message size and coupling to the model's class layout across deploys are further costs. ## The pattern ```python # orders/tasks.py from celery import shared_task from .models import Order @shared_task def send_order_confirmation(order_id): try: order = Order.objects.select_related("customer").get(pk=order_id) except Order.DoesNotExist: return # cancelled and deleted before the worker ran if order.confirmation_sent_at: return # already sent; the task may run twice order.send_confirmation_email() ``` - **Re-fetch** with only what the task needs (`select_related`, `only()`). - **Handle `DoesNotExist`** as a normal outcome, not a crash. - **Guard against running twice** — a message can be delivered more than once, so check a field before sending. - **Keep arguments plain** — ids, strings and numbers; anything richer is converted by the caller and rebuilt by the task. ## The commit trap that primary keys expose Passing the primary key creates one more requirement: the worker, on its own database connection, must be able to **see** the row. If the view creates the order inside a transaction (an `atomic` block or `ATOMIC_REQUESTS`) and queues the task immediately, a fast worker can run `Order.objects.get()` before the commit and raise `DoesNotExist`. Queueing from `transaction.on_commit()` — or Celery's `delay_on_commit()` shortcut on Django projects — closes that gap. ## What to say in an interview - Messages cross a process boundary, so arguments must serialise; JSON is the default. - Primary keys are small, stable and let the task read current data. - Pickle "fixes" the error but introduces staleness and a security risk. - The task owns the missing-row and double-delivery cases, and the view owns queueing after commit.
- The confirmation email sometimes shows an outdated shipping address. What is the likely cause?The task received data captured at queue time instead of reading it at run time, for example a pickled instance or a dict of fields built in the view. Anything edited between queueing and execution is lost. Pass the primary key and read the order inside the task, so the email reflects the row as it is when sent.
- Why should the task tolerate running twice for the same order?Delivery can repeat, for example when a worker dies before acknowledging a message or a retry follows a timeout after the email was already sent. Checking a field such as `confirmation_sent_at` before sending, and setting it afterwards, makes the second run a no-op instead of a duplicate email.
saying these in an interview costs you the question
- Passes the Order model instance to delay() and expects it to arrive intact
- Switches the serializer to pickle to make instances work
- Assumes the worker sees the order exactly as it was when queued, and that this is desirable
- Treats Order.DoesNotExist in the task as a bug rather than a normal outcome
- Assumes each message is delivered exactly once