How do you handle background or long-running tasks in Django?
Never do them in the request-response cycle. Sending an email, generating a report, or calling a slow third-party API inside a view holds a worker process hostage and eventually times out for the user.
The standard answer is Celery with a broker such as Redis or RabbitMQ:
@shared_task(bind=True, max_retries=3)
def send_invoice(self, order_id):
order = Order.objects.get(pk=order_id)
try:
mailer.send(order.email, render_invoice(order))
except TransientError as exc:
raise self.retry(exc=exc, countdown=60)The view calls send_invoice.delay(order.id) and returns immediately.
The practices that matter:
- Pass ids, not objects. Serialising a model instance means the worker acts on a stale copy.
- Make tasks idempotent. A task can be delivered twice; running it twice must not send two invoices.
- Set retries with backoff, and a time limit, so a hung task does not occupy a worker forever.
- Dispatch after the transaction commits —
transaction.on_commit(lambda: task.delay(id))— or the worker may look up a row that has not been written yet. This is a classic race that is hard to reproduce.
Note: For simpler needs, mention django-q or a management command run by cron. Celery is powerful but it is real operational overhead.





