· via dev.to (home feed)
Django 6.0's Tasks API ships without a worker, complicating Celery migration plans
Django 6.0's built-in Tasks API standardizes how background jobs are declared and enqueued but leaves execution to outside infrastructure, a surprise for teams that planned to drop Celery.

Django ships the interface, not the engine
Django 6.0, released on December 3, 2025, includes a built-in Tasks framework for background work, and the most consequential detail, according to a hands-on upgrade report on dev.to, is what it leaves out. Django's own documentation is explicit that the framework includes no mechanism for running tasks; execution has to be handled by a separate process or service outside Django itself.
The API surface is compact: a function is decorated with @task, optionally given a queue name and priority, and enqueued by calling .enqueue(), which returns a TaskResult. Backends are configured through a TASKS settings block, mirroring the pattern of DATABASES and CACHES.
Two backends ship in the box. ImmediateBackend runs the function inline the moment it is enqueued; DummyBackend records the result and never runs anything. The dev.to report points out that Django's reference docs describe both as development and testing tools, and direct production deployments toward backends that supply a worker process and a durable queue.
The gaps a production queue would fill
Assessed against what real background infrastructure needs, the framework is deliberately incomplete. The report lists what is missing: no worker, no retry policy (a task that raises records its exception and traceback in TaskResult.errors and then stops), no periodic scheduling, no dead-letter queue, no concurrency controls, and no durable storage unless the chosen backend supplies it.
A run_after option accepts a timedelta or a timezone-aware datetime, and backends advertise what they support through capability flags such as supports_defer, supports_priority and supports_get_result. A one-shot delay is useful, the author notes, but it is not a cron replacement.
The scope was intentional. The framework follows DEP 14, written by Jake Howard, whose goal was to provide an interface and base implementation for long-running background tasks in Django. The interface landed in core in September 2025 and shipped with 6.0. Nor did the next release close the gap: according to the report, Django 6.1 added a kwargs passthrough on the decorator and made Task and TaskResult picklable, but brought no database backend, no worker command and no retries.
What teams are putting behind the interface
The report weighs three execution options:
- django_tasks_db's DatabaseBackend stores tasks as database rows, ships a db_worker management command plus a pruning command, and makes scheduled, completed and errored tasks visible in the Django admin. The trade-off is contention: every enqueue is a write and every poll is a query against the same database serving web requests.
- Celery remains attractive where a broker is already deployed, since it still provides retries with backoff, beat scheduling, routing, rate limits and chords, none of which the Django interface is positioned to grow.
- RQ sits in between: Redis-backed and far lighter than Celery, suited to services that have outgrown database polling but do not need workflow primitives.
The author's own outcome on a compliance SaaS project: Celery stayed for sync jobs that need retries and a schedule, while two fire-and-forget email paths moved to the new API on the database backend, an incremental shift rather than the full Celery removal originally planned.
Two pitfalls the API does not solve
Enqueueing inside transaction.atomic() remains a known failure mode: a worker on a different connection can pick up a job before the transaction commits and fail to find the row it needs. Django's documentation prescribes transaction.on_commit wrapped around the enqueue call using functools.partial. Celery users get a shortcut in delay_on_commit(), added in Celery 5.4, though it returns no task ID because nothing reaches the broker until the transaction finishes; the Django interface has no equivalent.
Task arguments and return values are serialized to JSON, so passing a datetime object raises a TypeError. The recommended pattern is to pass primary keys and ISO-format strings, then re-fetch the object inside the task.
Why it matters
For teams that expected a 6.0 upgrade to make Celery deletable in an afternoon, the consequence is that the execution half of the problem is still theirs to fund and operate. The report still argues the interface is worth adopting, on two grounds: application code stops naming the queue vendor, and tests can point TASKS at DummyBackend to assert on enqueued tasks with no broker, eager-mode flag or mocked call involved. Swapping the database backend for a Redis-backed one later becomes a settings change plus a worker process rather than a rewrite. The realistic path for most existing projects is incremental: adopt the interface where it pays off, and keep Celery where retries and scheduling earn their keep.
- #django
- #python
- #celery
- #background-tasks
- #web-framework