skip to content

Deferred Work API

Django 6.0's django.tasks defines work with @task and queues it with enqueue() through a TASKS backend, yet ships no worker. Interviewers ask what actually runs the task in production.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django 6.0 or later, how do you define a thumbnail job with the @task decorator and enqueue it from a view?

level: juniorimportance: should knowfreq 34%

answer

  1. a decorator from django.tasks
  2. module-level function
  3. the decorated name is not a function
  4. enqueue() returns a result object

basics

~20 s

Decorate a module-level function with @task from django.tasks, then call make_thumbnail.enqueue(photo.pk) in the view; enqueue() hands the call to the configured TASKS backend and returns a TaskResult instead of running the function in the view.

solid answer

~30 s

In Django 6.0+ I put `@task` from `django.tasks` on a module-level function in the app's `tasks.py`, for example `make_thumbnail(photo_id)`. The decorator returns a `Task` object, not a function, so the view calls `make_thumbnail.enqueue(photo.pk)`; calling `make_thumbnail(photo.pk)` directly raises `TypeError`. `enqueue()` validates the task against the backend named in the `TASKS` setting, stores the call with JSON-serialisable arguments, and returns a `TaskResult` with an `id` and a `status`. `aenqueue()` is the variant for async views. What runs it depends on the backend: the default `ImmediateBackend` runs it straight away in the same thread, and production needs a third-party backend with a worker.

code

python · 14 lines
python
from django.tasks import task

from .imaging import render_thumbnail
from .models import Photo


@task(queue_name="default")
def make_thumbnail(photo_id):
    render_thumbnail(Photo.objects.get(pk=photo_id))


# in a view, after photo = form.save():
# result = make_thumbnail.enqueue(photo.pk)
# result.id, result.status

go deeper

for a junior

Recall the three moves: @task on a module-level function, enqueue() with simple arguments, and the TaskResult that comes back.

for a middle

Explain what enqueue() validates, why arguments must survive a JSON round-trip, and why the default backend still runs the work inside the request.

for a senior

Make sure the task re-fetches its data by primary key and that a production backend with a worker is configured before relying on it for latency.

for a principal

Decide whether to standardise on django.tasks as the project's task API so the queue backend can be swapped without touching view code.

## What django.tasks is Django 6.0 added the **Tasks framework** in `django.tasks`. It gives Django a standard way to **define** a piece of work and **enqueue** it to run outside the request-response cycle. It deliberately does not include a **worker** — the process that picks up queued work and runs it. Django handles definition, validation, queuing and result handling; execution belongs to a **backend** configured in the `TASKS` setting. (Before 6.0 the same API was available as a separate backport package.) ## Defining the task ```python # photos/tasks.py from django.tasks import task from .models import Photo from .imaging import render_thumbnail @task def make_thumbnail(photo_id): photo = Photo.objects.get(pk=photo_id) render_thumbnail(photo) ``` Rules the framework enforces or recommends: - The function must be defined **at module level**. A nested function or a lambda raises `InvalidTask` ("Task function must be defined at a module level"), because a worker must be able to import it by path. - By convention tasks live in `tasks.py`; this is not enforced. - `@task` can be used bare or with options: `@task(priority=2, queue_name="images")`. - The decorator returns a **`Task` instance**, a frozen object that wraps the function. It is not callable. ## Enqueueing it from a view ```python # photos/views.py from django.shortcuts import redirect from .forms import PhotoForm from .tasks import make_thumbnail def upload_photo(request): form = PhotoForm(request.POST, request.FILES) if form.is_valid(): photo = form.save() make_thumbnail.enqueue(photo.pk) return redirect("photos:detail", pk=photo.pk) ... ``` `enqueue(*args, **kwargs)`: 1. Validates the `Task` against its backend (queue name, priority, deferral support). An invalid combination raises `InvalidTask`. 2. Builds a `TaskResult` whose arguments are normalised to JSON-compatible values; a model instance or `datetime` raises `TypeError` here. 3. Hands it to the backend and returns the `TaskResult`, which has an `id`, a `status` (`READY`, `RUNNING`, `FAILED` or `SUCCESSFUL`) and timestamps. In an `async def` view, use `await make_thumbnail.aenqueue(photo.pk)`. ## The mistake interviewers look for | Call | What happens | |---|---| | `make_thumbnail(photo.pk)` | `TypeError`: a `Task` object is not callable | | `make_thumbnail.enqueue(photo.pk)` | Queued through the configured backend | | `make_thumbnail.enqueue(photo)` | `TypeError`: a model instance is not JSON-serialisable | | `@task` on a function nested in the view | `InvalidTask`: must be module level | ## What happens next depends on TASKS With no `TASKS` setting, Django uses `ImmediateBackend`, which **runs the task straight away, in the same thread, inside `enqueue()`**. That is convenient while adopting the API, but the thumbnail is still rendered during the request. Real background execution needs a third-party backend that provides a durable queue and a worker process; you point `TASKS["default"]["BACKEND"]` at its import path, and the view code does not change. ## What enqueue() does not do for you The API is intentionally small, so several things a queue library might do implicitly are left to you or to the backend: - **It does not wait for the database transaction.** If the photo row was created in an open transaction, a worker on another connection may not see it yet; enqueueing after commit is the documented pattern. - **It adds no retries.** Django records failures on the `TaskResult`; whether a failed task is retried is a backend feature. - **It does not deduplicate.** Calling `enqueue()` twice for the same photo queues two thumbnail jobs, so the task itself should be safe to run twice (for example, overwriting the same thumbnail file). - **It does not pick a backend per environment.** The same code runs inline under `ImmediateBackend` in development and in a worker in production, which is why tests should also exercise the real serialisation rules. ## Summary - `@task` turns a module-level function into a `Task`. - `enqueue()` / `aenqueue()` queue it and return a `TaskResult`. - Pass primary keys, not objects. - The backend in `TASKS` decides whether anything actually runs in the background.

  • How would you enqueue the same task from an async Django view?
    Use `await make_thumbnail.aenqueue(photo.pk)`. It takes the same arguments and returns the same `TaskResult`; the backend base class runs the synchronous `enqueue()` through `sync_to_async()` unless the backend provides a native async implementation.
  • Why must a Django task function be defined at module level?
    A task is stored as a reference to its function's import path so that a separate worker process can import and call it. A nested function or lambda has no importable path, so the backend's validation raises `InvalidTask` when the `Task` is created.

saying these in an interview costs you the question

  • Calls the decorated task like a normal function and expects it to be queued
  • Believes Django 6.0 ships a worker that runs enqueued tasks
  • Passes the Photo model instance straight to enqueue()
  • Defines the task inside the view function
  • Assumes enqueue() returns the task function's return value
open as a page

In Django 6.x, what actually runs a task enqueued through django.tasks, and how do ImmediateBackend and DummyBackend differ?

level: middleimportance: should knowfreq 36%

basics

~20 s

Django 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.

open as a page

In Django's Tasks framework, how do you check a task's outcome later from another request using TaskResult and get_result()?

level: middleimportance: should knowfreq 24%

basics

~10 s

Store TaskResult.id, then call task.get_result(id) or the backend's get_result(id) and read status: READY, RUNNING, FAILED or SUCCESSFUL. return_value works only after success; errors holds failures. Built-in backends cannot fetch results across processes.

open as a page

Why does Django's Tasks framework reject a model instance passed to enqueue(), and what should a thumbnail task receive instead?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Task arguments and return values go through a JSON round-trip so another process can run the task, so a model instance or datetime raises TypeError. Pass the primary key and re-fetch the row inside the task, handling a deleted or uncommitted row.

open as a page

In Django's Tasks framework, how do priority, queue_name, run_after and takes_context change a task, and when is it rejected?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

priority, queue_name and takes_context are set in @task; run_after only through Task.using(), which returns a modified copy. The backend validates each: unsupported deferral or priority, unknown queues or a bad context signature raise InvalidTask.

open as a page