# Introduction to Django 6.0 Background Tasks

**Django 6.0 introduces native support for background tasks, eliminating the need for third-party libraries in most common scenarios**. The new `django.tasks` module provides a standardized API for running work outside the request-response cycle, making it easier to send emails, process files, and handle time-consuming operations without blocking your users.

This guide introduces you to Django's background tasks framework through practical examples. You'll learn the core concepts, set up a working project, and **build real tasks that run in the background**. By the end, you'll understand when to use Django's built-in tasks versus reaching for more complex solutions like Celery.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/JCRu3zyMz54" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## Prerequisites

Before starting, ensure you have Python 3.10 or later installed on your system. You'll also need basic familiarity with Django's views and models.

Verify your Python version with:

```command
python3 --version
```

```text
[output]
Python 3.11.5
```

## Understanding why background tasks matter

When someone clicks a submit button on your Django site, their browser sends a request, Django does all the work, and then sends a response back. The browser waits the whole time. This works well for quick operations, but it becomes a problem when the work takes longer.

Here is how a signup request might look if everything runs in the request:

* Create the user in the database, send a welcome email, generate a default profile picture, and notify your team’s Slack channel

Sending emails and talking to external services can be slow, and the user ends up staring at a loading spinner. In reality, they do not care when the email is sent. They just want to know their account exists so they can continue. Background tasks fix this by keeping the fast, essential work in the request and moving the slow, optional work to a separate worker process that runs in the background.


## How Django approached background tasks

For years, Django developers solved background work with third party tools, which led to many different patterns and setups. There was no single, standard way to do it inside Django itself.

Most teams ended up with something like this:

* Use Celery, Huey, or RQ with a message broker such as Redis or RabbitMQ, each with its own configuration and conventions

Developers repeatedly asked for a simpler, built in option for common tasks such as sending emails or processing uploads. Jake Howard’s Django Enhancement Proposal suggested a small, standard API for tasks, with swappable backends. Django 6.0 turned this idea into reality with the `django.tasks` module, giving projects a shared, native way to define and enqueue background work.


## What Django’s task framework actually does

To use `django.tasks` effectively, it helps to see it as a thin layer that defines how you describe work, not how that work is executed. Django focuses on the API and leaves the heavy lifting to external workers and backends.

In practice, the framework gives you:

* A standard way to define tasks, enqueue them, and hand them off to pluggable backends such as a database queue, Redis, or other systems

This keeps Django small while giving you a clear path to offload simple tasks without jumping straight into a full distributed system. If your project later needs advanced features such as task chaining or complex routing, you can still bring in tools like Celery. Django’s task framework is a solid baseline that covers most use cases and leaves the door open for more powerful solutions when you need them.

## Setting up your Django project

Create a new directory for your project and navigate into it:

```command
mkdir django-tasks-demo
```

```command
cd django-tasks-demo
```

Set up a virtual environment to isolate your project dependencies:

```command
python3 -m venv venv
```

Activate the virtual environment:

```command
source venv/bin/activate
```

On Windows, use `venv\Scripts\activate` instead.

Install Django 6.0 and the database backend package:

```command
pip install Django django-tasks
```

Create a new Django project:

```command
django-admin startproject taskdemo .
```

The period at the end creates the project in the current directory instead of creating a subdirectory. Your project structure should look like this:

```text
.
├── manage.py
├── taskdemo
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── venv
```

Create an app to hold your tasks:

```command
python manage.py startapp accounts
```

Open `taskdemo/settings.py` and add your new app to `INSTALLED_APPS`:

```python
[label taskdemo/settings.py]
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
[highlight]
    'django_tasks',
    'django_tasks.backends.database',
    'accounts',
[/highlight]
]
```

Run migrations to set up your database:

```command
python manage.py migrate
```

```text
[output]
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, django_tasks, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  ...
  Applying django_tasks.0001_initial... OK
```

## Defining your first background task

Create a new file for your tasks:

```command
touch accounts/tasks.py
```

Open the file and define a task to send welcome emails:

```python
[label accounts/tasks.py]
from django.core.mail import send_mail
from django.tasks import task

@task
def send_welcome_email(user_email, username):
    subject = 'Welcome to TaskDemo'
    message = f'Hi {username}, thanks for joining TaskDemo!'
    from_email = 'noreply@taskdemo.com'
    
    send_mail(subject, message, from_email, [user_email])
    print(f'Welcome email sent to {user_email}')
```

The `@task` decorator marks this function as a background task. Django can now execute it outside the normal request-response cycle. The function looks like any other Python function, which keeps your code simple and testable.

The decorator accepts optional arguments to control task behavior:

```python
[label accounts/tasks.py]
from django.tasks import task

@task(priority=10, queue_name='emails')
def send_welcome_email(user_email, username):
    # task implementation
    pass
```

The `priority` argument ranges from -100 to 100, with higher numbers running first. The `queue_name` argument lets you separate different types of work into dedicated queues.

## Configuring the task backend

Django's task framework needs a backend to store and execute tasks. The database backend works well for most applications and doesn't require additional infrastructure like Redis or RabbitMQ.

Add the database backend configuration to your settings:

```python
[label taskdemo/settings.py]
TASKS = {
    'default': {
        'BACKEND': 'django_tasks.backends.database.DatabaseBackend',
        'QUEUES': ['default', 'emails', 'images'],
    }
}
```

The `QUEUES` setting defines which queue names your tasks can use. Django validates queue names when you enqueue tasks, catching typos early.

## Configuring email for testing

Before testing your view, configure Django to print emails to the console instead of trying to send them through an SMTP server. Add this to your settings file:

```python
[label taskdemo/settings.py]
# Add at the end of the file
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
```

This backend prints email content to your terminal, which is perfect for development and testing.

## Enqueuing tasks from views

Create a view that registers users and sends welcome emails in the background:

```python
[label accounts/views.py]
from django.http import HttpResponse
from django.contrib.auth.models import User
from django.views.decorators.csrf import csrf_exempt
from django.db import IntegrityError
from .tasks import send_welcome_email

@csrf_exempt
def register(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        email = request.POST.get('email')
        password = request.POST.get('password')
        
        try:
            user = User.objects.create_user(
                username=username,
                email=email,
                password=password
            )
            
            result = send_welcome_email.enqueue(
                user_email=user.email,
                username=user.username
            )
            
            print(f'Task ID: {result.id}')
            
            return HttpResponse(f'Registration successful! Task ID: {result.id}')
        except IntegrityError:
            return HttpResponse('Username already exists. Please choose another.', status=400)
    
    return HttpResponse('Send POST request to register')
```

The `@csrf_exempt` decorator allows testing with curl by bypassing Django's CSRF protection. In production, you'd handle CSRF tokens properly through forms or API authentication.

The `.enqueue()` method adds the task to your configured backend. The view returns immediately without waiting for the email to send. Notice how you pass keyword arguments that match your task function's parameters.

The `enqueue()` method returns a `TaskResult` object containing a unique task ID that you can store to check the task's status later.

Add a URL pattern for your view:

```python
[label taskdemo/urls.py]
from django.contrib import admin
from django.urls import path
[highlight]
from accounts import views
[/highlight]

urlpatterns = [
    path('admin/', admin.site.urls),
[highlight]
    path('register/', views.register),
[/highlight]
]
```

## Running the development server

Start Django's development server to test your view:

```command
python manage.py runserver
```

```text
[output]
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 07, 2025 - 15:30:22
Django version 6.0, using settings 'taskdemo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

Open another terminal, activate your virtual environment, and send a test request:

```command
curl -X POST http://127.0.0.1:8000/register/ \
  -d "username=alice" \
  -d "email=alice@example.com" \
  -d "password=secret123"
```

```text
[output]
Registration successful! Task ID: d3Lw462fPX3rEwTC0YJfn76mjc8ZlEr4
```

Check your server terminal. You'll see the email content printed because you configured the console email backend:

```text
[output]
Task id=d3Lw462fPX3rEwTC0YJfn76mjc8ZlEr4 path=accounts.tasks.send_welcome_email state=RUNNING
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Subject: Welcome to TaskDemo
From: noreply@taskdemo.com
To: alice@example.com

Hi alice, thanks for joining TaskDemo!
-------------------------------------------------------------------------------
Welcome email sent to alice@example.com
Task id=d3Lw462fPX3rEwTC0YJfn76mjc8ZlEr4 path=accounts.tasks.send_welcome_email state=SUCCESSFUL
```

The database backend stores the task and executes it. For true background processing where tasks don't block the request-response cycle, you need to run a separate worker process.

## Running the task worker

The database backend stores tasks in your database but executes them in the same process by default. To run tasks in a separate background worker, stop your development server with `Ctrl+C` and start the worker:

```command
python manage.py db_worker
```

```text
[output]
Starting database task worker...
Polling for tasks every 1.0 seconds
Press Ctrl+C to stop
```

The worker runs in a loop, checking for new tasks and executing them as they arrive. Keep this running in a separate terminal while you test your application.

In your original terminal, start the development server again:

```command
python manage.py runserver
```

Now when you register a user with a different username, the view returns immediately and the worker picks up the email task:

```command
curl -X POST http://127.0.0.1:8000/register/ \
  -d "username=bob" \
  -d "email=bob@example.com" \
  -d "password=secret123"
```

You'll see output in the worker terminal showing the task execution:

```text
[output]
Executing task: send_welcome_email
Content-Type: text/plain; charset="utf-8"
Subject: Welcome to TaskDemo
From: noreply@taskdemo.com
To: bob@example.com

Hi bob, thanks for joining TaskDemo!
-------------------------------------------------------------------------------
Welcome email sent to bob@example.com
Task completed successfully
```

The view returns instantly while the worker handles the email in the background. This separation means your users never wait for slow operations.

## Checking task status

You can query task status using the task ID returned by `enqueue()`. Create a view that checks whether a task finished:

```python
[label accounts/views.py]
[highlight]
from django.http import HttpResponse, JsonResponse
[/highlight]
from django.contrib.auth.models import User
from django.views.decorators.csrf import csrf_exempt
from django.db import IntegrityError
[highlight]
from django.tasks import default_task_backend
[/highlight]
from .tasks import send_welcome_email

@csrf_exempt
def register(request):
    ...    
    return HttpResponse('Send POST request to register')

[highlight]
def task_status(request, task_id):
    try:
        result = default_task_backend.get_result(task_id)
        
        return JsonResponse({
            'id': result.id,
            'status': result.status.name,
            'finished': result.is_finished,
            'attempts': result.attempts,
        })
    except Exception:
        return JsonResponse({'error': 'Task not found'}, status=404)
[/highlight]
```

Add the URL pattern:

```python
[label taskdemo/urls.py]
from django.contrib import admin
from django.urls import path
from accounts import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', views.register),
[highlight]
    path('task/<str:task_id>/', views.task_status),
[/highlight]
]
```

After registering a user and getting a task ID, you can check its status:

```command
curl http://127.0.0.1:8000/task/<your_task_id>/
```

```text
[output]
{"id": "d3Lw462fPX3rEwTC0YJfn76mjc8ZlEr4", "status": "SUCCESSFUL", "finished": true, "attempts": 1}
```

The status changes from `READY` to `RUNNING` to `SUCCESSFUL` as the worker processes the task. If you check the status immediately after enqueuing a task, you might catch it in the `READY` or `RUNNING` state before it completes.

Register another user and quickly check the task status:

```command
curl -X POST http://127.0.0.1:8000/register/ \
  -d "username=charlie" \
  -d "email=charlie@example.com" \
  -d "password=secret123"
```

```text
[output]
Registration successful! Task ID: x9Y2mKpQrS5tVwNzL8hJfD3gB6nC4eA1
```

Immediately check the status:

```command
curl http://127.0.0.1:8000/task/<your_task_id>/
```

If you're fast enough, you might see:

```text
[output]
{"id": "x9Y2mKpQrS5tVwNzL8hJfD3gB6nC4eA1", "status": "RUNNING", "finished": false, "attempts": 1}
```

Wait a moment and check again to see it completed:

```text
[output]
{"id": "x9Y2mKpQrS5tVwNzL8hJfD3gB6nC4eA1", "status": "SUCCESSFUL", "finished": true, "attempts": 1}
```

This status checking mechanism lets you build user interfaces where people can monitor their background jobs, similar to upload progress bars or report generation status indicators.


## Final thoughts

**Django 6.0’s task framework gives you a built-in, lightweight way to move slow work out** of the request-response cycle. In this guide, you:

* Set up a Django project, defined a background task, configured the database backend, ran a worker, and exposed an endpoint to check task status

**For most everyday jobs like sending emails or processing uploads, `django.tasks` is all you need**. When your project grows and you require advanced workflows or large-scale processing, you can still step up to tools like Celery without throwing away what you learned here.

