Background tasks

Django · Advanced · 12 min

Slow work such as emails, exports and webhooks must leave the request cycle to keep pages fast.

Requests should finish in milliseconds, so slow or unreliable work (sending email, generating reports, calling webhooks) belongs in background tasks.

Django 6.0 adds a built-in Tasks framework (django.tasks) that standardizes how you define and enqueue work:

@task
def send_welcome_email(user_id): ...

send_welcome_email.enqueue(user.pk)

enqueue() returns a TaskResult with an id and status; get_result(id) looks it up later. Options such as priority, queue_name and run_after can be set on the decorator or with .using(...).

Backends are configured in the TASKS setting. Django ships only two:

  • ImmediateBackend (the default): runs the task synchronously during enqueue(), convenient in development
  • DummyBackend: stores results without running anything, useful in tests

Django does not include a worker. For real background execution you need a backend from a third-party package (such as the django-tasks reference implementation with its database backend and worker command), or you keep using an established queue such as Celery or RQ with its own decorator and workers.

Rules that apply to any task system:

  • pass IDs, not model instances: arguments must be serializable (JSON for django.tasks), and the row may change before the task runs
  • enqueue after commit: transaction.on_commit(lambda: task.enqueue(order.pk))
  • make tasks idempotent, because retries and duplicates happen
  • tasks must be module-level functions so workers can import them

Example

# settings.py
TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}
# production: a worker-based backend from a third-party package

# accounts/tasks.py
from django.core.mail import send_mail
from django.tasks import task

@task(priority=10)
def send_welcome_email(user_id):
    from accounts.models import User
    user = User.objects.get(pk=user_id)          # fetch fresh data in the task
    send_mail("Welcome!", f"Hi {user.first_name}", None, [user.email])
    return user.email

# accounts/views.py
from django.db import transaction

def signup_done(user):
    transaction.on_commit(lambda: send_welcome_email.enqueue(user.pk))

# shell
result = send_welcome_email.enqueue(1)
print(result.status, result.return_value)        # SUCCESSFUL ann@example.com  (ImmediateBackend)

Remember

  • Django 6.0: @task defines a task, .enqueue() schedules it
  • Built-in backends: ImmediateBackend (runs inline) and DummyBackend (tests); workers come from third-party backends
  • Pass IDs, enqueue on commit, make tasks idempotent

Watch out for

  • Passing model instances or other non-serializable objects as task arguments.

Check yourself

  1. With the default ImmediateBackend, when does an enqueued task run?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/tasks/

Builds on

Unlocks

Nothing builds on this yet.