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 duringenqueue(), convenient in developmentDummyBackend: 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.