Django Tasks Framework: A Complete Guide to Background Task Execution in Django 6.0

Django 6.0 introduces the Tasks framework, a built‑in system for defining and queuing background work outside the request–response cycle. This article explains how Tasks work, how to configure backends, how to define and enqueue tasks, how context works, and how to integrate third‑party worker systems for production environments.

Django Tasks, background jobstask backend, ImmediateBackend, DummyBackend, BaseTaskBackendasync task backend, Django 6.0 tasks

~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

  1. Django enqueues a Task
  2. The Task is stored in the backend
  3. A Worker claims the Task
  4. The Worker executes it
  5. 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 → becomes list → 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