In Django 6.x, what actually runs a task enqueued through django.tasks, and how do ImmediateBackend and DummyBackend differ?
answer
- Django ships no worker
- the TASKS setting default
- one runs inline, one never runs
- third-party backends with durable queues
basics
~20 sDjango ships no worker. The default ImmediateBackend runs each task synchronously inside enqueue(); DummyBackend stores results and never runs them. Both are for development and tests; production needs a third-party backend with a durable queue and worker process.
solid answer
~40 s`django.tasks` only defines and queues work; execution belongs to the backend named in `TASKS`. The default is `{"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}`, which runs the task right away in the caller's thread, catches its exception, and returns a `TaskResult` already `SUCCESSFUL` or `FAILED`. `DummyBackend` executes nothing: results stay `READY` in its `results` list, which you can inspect and `clear()` in tests. Neither supports retrieving results from another process, and `ImmediateBackend` rejects `run_after`. For real background work you install a third-party backend that supplies a durable queue and a worker, and point `BACKEND` at its import path. So an interview answer to 'what runs it' is: not Django.
code
python · 6 linesTASKS = {
"default": {
"BACKEND": "django.tasks.backends.dummy.DummyBackend",
"QUEUES": ["default", "images"],
}
}go deeper
Remember that Django only queues tasks: the default backend runs them immediately and something else must run them in production.
Contrast ImmediateBackend and DummyBackend: inline execution with caught errors versus recorded results that stay READY, plus the supports_* flags each sets.
Plan the move to a production backend: durable queue, a worker process to deploy and monitor, and tests that use DummyBackend to assert enqueueing.
Judge whether the backend-neutral API is worth adopting over an existing queue library, given that execution guarantees depend entirely on the chosen backend.
## The division of labour The **Tasks framework** (Django 6.0) splits background work into parts: - **Task** — a module-level function decorated with `@task`. - **Backend** — a class inheriting `BaseTaskBackend`, configured in `TASKS`, that decides how and where a queued task is stored and run. - **Queue store** — wherever the backend keeps queued tasks. - **Worker** — a process that claims queued tasks, runs them and writes the outcome back. Django provides the first two pieces' APIs and two simple backends. It **does not provide a worker**. The docs are explicit: the built-in backends are for development and testing only, and production should use backends that supply a worker process and a durable queue. ## The TASKS setting `TASKS` looks like `DATABASES` or `CACHES`: a dict of aliases. ```python TASKS = { "default": { "BACKEND": "django.tasks.backends.immediate.ImmediateBackend", "QUEUES": ["default"], } } ``` That `default` entry is also the value in `global_settings.py`, so a project that never mentions `TASKS` uses `ImmediateBackend`. `QUEUES` lists the queue names tasks may use (a task on any other queue raises `InvalidTask`, unless `QUEUES` is `[]`), and `OPTIONS` carries backend-specific settings. Other aliases are reached with `task_backends["alias"]` or `@task(backend="alias")`. ## The two built-in backends | | `ImmediateBackend` | `DummyBackend` | |---|---|---| | Runs the task | Yes, **inside `enqueue()`**, same thread | **Never** | | Status after `enqueue()` | `SUCCESSFUL` or `FAILED` | `READY`, forever | | Task exception | Caught, stored in `errors`, logged at error level | Not applicable | | `run_after` (deferral) | Not supported → `InvalidTask` | Accepted | | Async task functions | Supported | Supported | | Priority | Accepted, has no effect | Accepted, has no effect | | `get_result()` from elsewhere | Not supported (`NotImplementedError`) | Only results held in the same process | | Intended use | Adopting the API before infrastructure exists; tests that want the effect | Tests asserting *that* something was enqueued | Two consequences surprise people: 1. With the default backend, a slow thumbnail task **still slows the request** — nothing moved to the background. 2. With the default backend, a task that raises **does not raise in the view**. `ImmediateBackend` catches the exception, marks the result `FAILED`, stores a `TaskError` and logs it on the `django.tasks` logger. Code that assumed an exception would surface keeps going. ## Using DummyBackend in tests ```python from django.tasks import default_task_backend from django.test import TestCase, override_settings @override_settings(TASKS={"default": {"BACKEND": "django.tasks.backends.dummy.DummyBackend"}}) class UploadTests(TestCase): def test_upload_enqueues_thumbnail(self): self.client.post("/photos/upload/", {...}) self.assertEqual(len(default_task_backend.results), 1) ``` Django resets its backend handler when `TASKS` changes through `setting_changed`, so the override takes effect. `default_task_backend.clear()` empties the list between tests. ## Going to production A production backend: - persists queued tasks durably (a database table, a broker), - ships a worker command you run as a separate process, - usually sets `supports_defer`, `supports_get_result` and `supports_priority` to `True`. You install it, point `BACKEND` at its import path, and run its worker alongside the web process. View code calling `enqueue()` does not change, which is the main point of the API. Which backend to choose is an infrastructure decision; the list lives on the Django community ecosystem page rather than in Django itself. ## Where Celery fits A project already on Celery keeps using Celery's own task API and worker; `django.tasks` does not configure or replace it. The two are separate APIs, and wiring Celery into Django is its own topic.
- With the default TASKS setting, a thumbnail task raises inside enqueue(). What does the view see?Nothing, unless it checks the result. `ImmediateBackend` catches the exception, sets the `TaskResult` status to `FAILED`, appends a `TaskError` with the exception class path and traceback, and logs it at error level on `django.tasks`. `enqueue()` returns normally, so the view continues.
- Why can't ImmediateBackend retrieve a result with get_result() later?It runs the task inline and does not store the result anywhere another request or process could find it, so it keeps `supports_get_result` false and calling `get_result()` raises `NotImplementedError`. Retrieval needs a backend with a shared result store.
django.tasks is a standard order slip. ImmediateBackend has the waiter cook the dish himself before taking the next order; DummyBackend files every slip in a drawer that nobody opens; a production backend is a real kitchen with cooks, which Django does not supply.
saying these in an interview costs you the question
- Believes the default TASKS backend runs tasks in a background thread
- Expects DummyBackend to execute tasks eventually
- Assumes Django 6.0 includes a worker management command
- Thinks a failing task under ImmediateBackend raises in the view
- Deploys ImmediateBackend to production expecting faster responses