Django

Django Background Tasks👨‍💻

Django 6.0 (released December 2025) introduced a built-in background tasks framework in django.tasks. For years, running work outside the request-response cycle meant reaching for Celery, Django-Q, or similar third-party libraries. Now Django ships with a standard API for defining and enqueueing tasks, giving the ecosystem a common interface that any backend can implement.

The core idea is simple: you decorate a function with @task, then call .enqueue() to send it to a queue. An external worker process picks it up and executes it. Django handles the API and queuing; the worker infrastructure is pluggable via backends. Two built-in backends cover development and testing, while third-party backends handle production workloads with durable queues and worker management.

Key Takeaways

  • 1The `@task` decorator from `django.tasks` turns any function into a background task — the function can no longer be called directly, only via `.enqueue()`
  • 2All task arguments and return values must be JSON-serializable — no datetime objects, model instances, or tuples without explicit conversion
  • 3Django 6.0 ships with two built-in backends: `ImmediateBackend` (executes synchronously, for development) and `DummyBackend` (stores without executing, for testing)
  • 4Production deployments require a third-party backend that provides durable queues and worker processes — Django defines the API, not the infrastructure
  • 5Every `.enqueue()` call returns a `TaskResult` with status tracking (`READY`, `RUNNING`, `SUCCESSFUL`, `FAILED`), return values, and error information
  • 6Tasks support priority levels, named queues, and a `takes_context` option that provides attempt count and task result metadata inside the function

Master django background tasks

Take the Django Performance course with hands-on lessons and challenges.

Examples

Basic @task decorator and enqueue

python

The @task decorator wraps the function so it can only be executed via .enqueue(). Calling email_users() directly will raise an error. The enqueue() call returns a TaskResult that you can use to track the task's progress and retrieve its return value later.

Enqueue with priority, queue name, and task context

python

The @task decorator accepts priority (higher runs first), queue_name (for routing to specific workers), and takes_context (injects a TaskContext as the first argument). The .using() method creates a modified copy of the task without changing the original definition, useful for one-off overrides.

TASKS setting configuration

python

The TASKS setting maps backend names to their implementation classes. ImmediateBackend runs tasks synchronously (great for local development). DummyBackend stores tasks without executing them (great for tests — you can inspect default_task_backend.results). Production requires a third-party backend that manages worker processes and durable queues.

Error handling and result inspection

python

TaskResult provides status, return_value, and errors attributes. Call .refresh() (or await .arefresh()) to update the status from the backend. Failed tasks expose TaskError objects with the exception class and a traceback string. The DummyBackend is designed for testing: it collects results without running them, and .clear() resets the state.

Transaction safety and comparison with Celery

python

When creating database records and enqueueing tasks in the same flow, always use transaction.on_commit() to ensure the worker sees committed data. This is a common pitfall with any task queue. The Celery comparison shows that Django's API is intentionally similar (.enqueue mirrors .delay), but Django's framework is simpler to set up for straightforward background work.

Common Mistakes

Mistake:

Assuming tasks run immediately after calling `.enqueue()` in production — the task is queued, not executed inline

Fix:

Tasks are picked up by external worker processes. In development, `ImmediateBackend` runs them synchronously, but this is not the production behavior. Always design your code to handle the delay between enqueueing and execution.

Mistake:

Not configuring a production-ready backend — the built-in `ImmediateBackend` and `DummyBackend` are not suitable for production

Fix:

Install a third-party backend (like `django-tasks-database`) that provides durable queues and worker processes. The built-in backends are for development and testing only. Without a proper backend, tasks either block the request or never run.

Mistake:

Passing non-JSON-serializable arguments like `datetime` objects, Django model instances, or tuples to `.enqueue()`

Fix:

Convert arguments to JSON-safe types before enqueueing: use ISO strings for dates (`dt.isoformat()`), primary keys for model instances (`user.id` instead of `user`), and lists instead of tuples. All arguments must survive a `json.dumps()` / `json.loads()` round trip.

Mistake:

Enqueueing tasks inside a database transaction without `transaction.on_commit()` — the worker may execute before the transaction commits

Fix:

Wrap the `.enqueue()` call with `transaction.on_commit(partial(my_task.enqueue, ...))` so the task is only queued after the database changes are visible. This prevents race conditions where the worker queries data that doesn't exist yet.

Best Practices

  • Define tasks in a `tasks.py` file within each Django app — this is the convention the framework encourages and makes tasks easy to discover
  • Always use `transaction.on_commit()` when enqueueing tasks after database writes to avoid race conditions with workers
  • Keep task arguments simple and JSON-serializable — pass IDs and primitive values, then query the database inside the task for full objects
  • Use the `DummyBackend` in tests to assert tasks were enqueued without actually executing them — inspect `default_task_backend.results` and call `.clear()` between tests
  • Set meaningful `queue_name` values to separate fast tasks (emails, notifications) from slow tasks (report generation, data processing) so they can be scaled independently
  • For complex workflows requiring task chaining, retries with backoff, periodic scheduling, or canvas primitives, Celery remains the better tool — Django's built-in tasks are designed for simple, fire-and-forget use cases

Summary

Django 6.0 introduced a built-in background tasks framework in `django.tasks` that provides a standard API for defining and enqueueing background work. The `@task` decorator marks functions for background execution, and `.enqueue()` sends them to a queue for processing by external workers. Two built-in backends cover development (`ImmediateBackend`) and testing (`DummyBackend`), while production deployments use third-party backends with durable queues. All task arguments must be JSON-serializable, and tasks should be enqueued with `transaction.on_commit()` when paired with database writes. For simple background work like sending emails or generating reports, this replaces the need for Celery. For complex workflows with chaining, retries, and periodic scheduling, Celery remains the established choice.

Practice Django with hands-on challenges

Learn django background tasks hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Django with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.