Django 6.0 Standardized Background Tasks — And Deliberately Left Out the Worker
Every Django team eventually hits the same wall. A view needs to send an email, generate a PDF, resize an image, or push a webhook — and…
Django 6.0 Standardized Background Tasks — And Deliberately Left Out the Worker

The new django.tasks framework is not the Celery replacement it looks like. Its real value is a boundary most Django codebases never had.
Every Django team eventually hits the same wall. A view needs to send an email, generate a PDF, resize an image, or push a webhook — and doing it inside the request makes the response slow and fragile. The textbook answer for a decade has been Celery: add a broker (Redis or RabbitMQ), run a worker process, wire up a result backend, and move the slow work out of the request path.
That answer works. It also means a three-line feature now drags in a distributed system you have to deploy, monitor, secure, and reason about. For a small service, the operational cost of Celery often dwarfs the problem it solves. Django 6.0 finally changed the default starting point — but not in the way most headlines suggested.
The Problem: Every Project Reinvents the Same Boundary
Before Django 6.0, there was no standard way to say “run this later.” Each project invented its own convention. Some used Celery’s @shared_task. Some used RQ. Some used a homegrown threading.Thread that quietly died on the next deploy. Some just called the slow function inline and hoped the request finished before the timeout.
The deeper issue is coupling. When your business logic imports from celery import shared_task and calls .delay(), your domain code is now married to a specific queue technology. Swapping Celery for something lighter — or heavier — means touching every call site. Testing means either running a broker or mocking Celery internals. The queue leaks into places that should not know a queue exists.
The Solution: A Task Contract, Not a Task Runner
Django 6.0 ships django.tasks, a built-in framework for defining and enqueueing background work. The API is deliberately small:
from django.tasks import task
@task
def send_welcome_email(user_id: int) -> None:
user = User.objects.get(pk=user_id)
send_email(user.email, subject="Welcome", body=render_welcome(user))
You do not call send_welcome_email(user_id) anymore. You enqueue it:
send_welcome_email.enqueue(user_id=user.id)
Calling the function directly is intentionally blocked. That single design decision eliminates a whole category of bugs where a “background” task silently runs in the web process because someone forgot .delay(). The intent is now explicit in the code and enforced by the framework.
Here is the part that surprises people. Django 6.0 ships two backends — an immediate backend that runs the task synchronously (for development) and a dummy backend that records calls without running them (for tests). Neither is meant for production. There is no bundled worker, no scheduler, no retry engine, no persistence, and no delivery guarantee. django.tasks defines the work and hands it to a backend; something outside Django has to actually run it.
For production you still choose an execution layer: the django-tasks package provides a database-backed worker that needs no broker, or you can point the same API at Celery. The framework standardizes the front door — @task and .enqueue() — and leaves the engine room to you.
The Trade-offs: What You Gain, What You Still Own
The gain is decoupling, and it is real. Your application code now depends on Django’s task contract instead of a vendor’s decorators and imports. That makes the queue a swappable backend. A new service can start with a database-backed worker — no Redis, no separate broker to run — and graduate to Celery and Redis when throughput demands it, without rewriting a single call site. Tests get simpler too: swap in the dummy backend and assert that a task was enqueued, with no broker in sight.
But the framework’s minimalism is a two-sided coin. Retries, exponential backoff, idempotency, rate limiting, and scheduling are still entirely your responsibility. Django standardized the enqueue; it did not standardize the hard operational guarantees that make background work trustworthy. If you were hoping to delete your Celery beat schedule, you cannot — there is no recurrence in the box.
Performance is a trade-off, not a free lunch. A database-backed worker is wonderfully simple to operate, but polling a table for pending jobs is not free, and it will not match a dedicated broker under high throughput. For a queue processing millions of jobs a day, Redis or RabbitMQ still wins.
There is also a subtler risk. An abstraction that hides which backend you are running can also hide very different failure semantics. An at-least-once Redis worker and a transactional database-backed worker behave differently when a job fails mid-flight, when a deploy interrupts a worker, or when the same task is enqueued twice. A uniform interface does not give you uniform guarantees, and treating it as if it does is how you ship a data-corruption bug that only appears under load.
How I Would Use It
Treat django.tasks as a decoupling tool, not a Celery replacement. Write your business logic against the task interface so it stops caring where and how work runs. Then choose the execution layer as a separate, deliberate decision driven by durability and throughput requirements — not by whatever you happened to import first.
For most small and medium services, starting with the database-backed worker is the right call: fewer moving parts, no broker to babysit, and a clean upgrade path. When you genuinely outgrow it, you move to Celery underneath the same interface, and your domain code never notices. The value of Django 6.0’s Tasks framework was never the worker it left out. It was forcing the boundary between “define the work” and “run the work” to finally, explicitly exist.
If you are on Django 6.0, are you adopting the new task interface even while keeping Celery underneath — or waiting until the worker story matures?
메타데이터
- post_id
- f59fea3acbd2
- slug
- django-6-0-standardized-background-tasks-and-deliberately-left-out-the-worker-f59fea3acbd2
- url
- https://medium.com/@alirazmjoie/django-6-0-standardized-background-tasks-and-deliberately-left-out-the-worker-f59fea3acbd2
- canonical_url
- https://medium.com/@alirazmjoie/django-6-0-standardized-background-tasks-and-deliberately-left-out-the-worker-f59fea3acbd2
- author_url
- https://medium.com/@alirazmjoie
- status
- ok
- fetched_at
- 2026-07-09 03:40:04