~6 min read • Updated Mar 15, 2026
Introduction
Modern web applications often need to perform work outside the request–response cycle—sending emails, generating reports, syncing data, or processing media. Django 6.0 introduces the Tasks framework, a built‑in system for defining and queuing background work.
The framework handles:
- Task definition
- Validation
- Queuing
- Result handling
It does not execute tasks itself. Execution must be handled by an external worker or service.
Background Task Fundamentals
When background work is needed, Django creates a Task and stores it in a Queue Store. A worker process—running outside Django—retrieves tasks, executes them, and stores results.
Task lifecycle
- Django enqueues a Task
- The Task is stored in the backend
- A Worker claims the Task
- The Worker executes it
- The result is saved back to the backend
Configuring a Task Backend
The backend determines:
- Where tasks are stored
- How workers retrieve them
- How results are saved
Backends are configured in the TASKS setting:
TASKS = {
"default": {
"BACKEND": "path.to.backend",
}
}
Most applications only need one backend, but multiple are supported.
Immediate Execution Backend
This is the default backend. It executes tasks immediately—synchronously—rather than in the background.
Useful for:
- Early development
- Local testing
- Projects without worker infrastructure
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"
}
}
Dummy Backend
The DummyBackend does not execute tasks at all. It stores results in memory and leaves them in the READY state forever.
Useful for:
- Testing
- Development environments
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.dummy.DummyBackend"
}
}
Access stored results:
from django.tasks import default_task_backend
my_task.enqueue()
len(default_task_backend.results)
Clear results:
default_task_backend.clear()
Third‑Party Backends
Django’s built‑in backends are for development only. Production systems require:
- A durable queue
- A worker process
- Reliable execution
Popular options include:
- Redis‑based task queues
- Cloud task systems
- Message brokers
Configure them using their import path:
TASKS = {
"default": {
"BACKEND": "path.to.production.backend",
}
}
Asynchronous Support
Task backends may implement async variants of all methods.
Async versions are prefixed with a (e.g., enqueue() → aenqueue()).
Retrieving Backends
from django.tasks import task_backends
task_backends["default"]
task_backends["secondary"]
Shortcut:
from django.tasks import default_task_backend
Defining Tasks
Tasks are defined using the @task decorator on a module‑level function.
Example
from django.core.mail import send_mail
from django.tasks import task
@task
def email_users(emails, subject, message):
return send_mail(
subject=subject,
message=message,
from_email=None,
recipient_list=emails,
)
The decorator returns a Task instance.
Customizing Task Attributes
@task(priority=2, queue_name="emails")
def email_users(...):
...
By convention, tasks live in tasks.py, but this is not required.
Task Context
Tasks can receive execution context using takes_context=True.
Example
import logging
from django.tasks import task
logger = logging.getLogger(__name__)
@task(takes_context=True)
def email_users(context, emails, subject, message):
logger.debug(
f"Attempt {context.attempt} to send email. Task result id: {context.task_result.id}."
)
...
Conclusion
The Django Tasks framework provides a clean, powerful way to define and queue background work. While Django does not execute tasks itself, it integrates seamlessly with external workers and queue systems. With support for async backends, task context, and flexible configuration, the framework is a strong foundation for scalable background processing in Django applications.
Modifying Tasks Before Enqueueing
Sometimes you need to adjust a Task’s parameters—such as increasing its priority—before enqueueing it.
Task instances are immutable, meaning they cannot be modified directly.
Instead, Django provides the using() method to create a modified copy.
Example
>>> email_users.priority
0
>>> email_users.using(priority=10).priority
10
The original Task remains unchanged, ensuring safe reuse.
Enqueueing Tasks
To add a Task to the queue store, call enqueue().
Arguments are passed normally:
result = email_users.enqueue(
emails=["[email protected]"],
subject="You have a message",
message="Hello there!",
)
This returns a TaskResult, which can later be used to retrieve the Task’s outcome.
Async Enqueueing
result = await email_users.aenqueue(...)
JSON Serialization Requirements
Task arguments and return values must be JSON‑serializable. This means:
datetime→ ❌tuple→ becomeslist→ may break logic- model instances → ❌
- complex objects → ❌
Example Error
>>> process_data.enqueue(datetime.now())
TypeError: Object of type datetime is not JSON serializable
Round‑trip JSON Pitfall
Consider this Task:
@task()
def double_dictionary(key):
return {key: key * 2}
Passing a tuple fails because JSON converts it to a list:
>>> result = double_dictionary.enqueue((1, 2, 3))
>>> result.status
FAILED
Transactions and Task Execution
Most backends run Tasks in separate processes with separate database connections. If a Task is enqueued inside a transaction that hasn’t committed yet, the worker may run it too early.
Problematic Example
with transaction.atomic():
Thing.objects.create(num=1)
my_task.enqueue(thing_num=1)
The Task may run before the object is committed.
Correct Approach: transaction.on_commit()
from functools import partial
from django.db import transaction
with transaction.atomic():
Thing.objects.create(num=1)
transaction.on_commit(partial(my_task.enqueue, thing_num=1))
Retrieving Task Results
Every TaskResult has a unique ID. You can retrieve the result later:
result = email_users.get_result(result_id)
Or retrieve it directly from the backend:
from django.tasks import default_task_backend
result = default_task_backend.get_result(result_id)
Async Variant
result = await email_users.aget_result(result_id)
Refreshing Task Results
A TaskResult reflects the state at the moment it was retrieved. If the Task finishes later, you must refresh it:
>>> result.status
RUNNING
>>> result.refresh() # or await result.arefresh()
>>> result.status
SUCCESSFUL
Accessing Return Values
If the Task completed successfully, you can access its return value:
>>> result.return_value
42
If the Task is still running or failed:
ValueError: Task has not finished yet
Error Handling
If a Task raises an exception, Django stores error information in result.errors.
Example
>>> result.errors[0].exception_class
Tracebacks are stored as strings:
>>> result.errors[0].traceback
Traceback (most recent call last):
...
TypeError: Object of type datetime is not JSON serializable
Conclusion
Django’s Tasks framework provides a robust system for managing background work—from modifying Task parameters and enqueueing them safely to handling transactions, retrieving results, and debugging failures.
By understanding JSON constraints, using transaction.on_commit(), and leveraging TaskResult effectively, you can build reliable and scalable background processing pipelines in Django.
Written & researched by Dr. Shahin Siami