Django 6.0 Tasks vs Celery: When Is the Built-In Task Framework Enough?
Django 6.0 finally gives Django applications a standard API for background tasks, but it does not make Celery obsolete.
The new __INLINE_CODE_0__ framework defines how tasks are declared, validated, queued and tracked. Django deliberately does not ship a production worker engine or durable production queue; its built-in __INLINE_CODE_1__ and __INLINE_CODE_2__ are for development and testing. A production deployment still needs external task infrastructure.
That changes the architecture decision. You no longer have to choose between “Django alone” and “Celery” as if they were equivalent products. The real choice is:
Do you need Django's standard task contract with a suitable production backend, or do you need Celery's mature execution platform, retries, scheduling, routing and workflow primitives?
This guide compares the two without the “Celery killer” headline and gives a practical decision framework for new and existing Django systems.
What did Django 6.0 actually add?
Django 6.0 introduced a built-in Tasks framework through __INLINE_CODE_0__.
The Django 6.0 release notes describe it as infrastructure for running work outside the HTTP request-response cycle. Django provides task definition, validation, queuing and result handling, while execution remains the responsibility of external worker infrastructure.
A basic task looks like:
from django.core.mail import send_mail
from django.tasks import task
@task
def send_welcome_email(email: str) -> int:
return send_mail(
subject="Welcome",
message="Thanks for joining.",
from_email=None,
recipient_list=[email],
)You enqueue it with:
send_welcome_email.enqueue(email="user@example.com")That API is valuable because application code can depend on Django's task abstraction rather than directly coupling every task definition to one queue product.
But there is an important boundary.
Django's own Tasks framework documentation says the framework does not provide the worker mechanism that executes production tasks.
A useful mental model is:
Django Tasks
= task contract + queue interface + task/result model
Production task backend
= durable queue + execution mechanism
Worker infrastructure
= processes/services that actually run the workDjango standardised the first layer. It did not silently bundle the other two.
Does Django 6.0 include a production task queue?
No.
This is the most important fact to understand before replacing an existing Celery deployment.
Django currently includes two built-in task backends:
| Backend | What it does | Production use? |
|---|---|---|
| __INLINE_CODE_0__ | Executes the task immediately in the current process | No, primarily development/testing |
| __INLINE_CODE_0__ | Stores task results for inspection but does not execute the task | No, development/testing |
The Django task reference explicitly says Django includes only development and testing backends and points production applications to external backends.
So this:
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
}
}does not make an HTTP request asynchronous.
If a view enqueues a 30-second task through __INLINE_CODE_0__, that task runs immediately in the same process.
For production, choose a backend that provides the durability and worker behaviour your application actually requires.
Django Tasks and Celery are not the same layer
This is why “Django Tasks vs Celery” can be a misleading comparison.
Celery is a distributed task execution system. It includes mature concepts around workers, brokers, retry policies, task routing, concurrency, periodic scheduling, workflow composition, monitoring and worker lifecycle.
The current Celery 5.6 user guide documents those operational capabilities.
Django Tasks is intentionally a narrower framework contract.
Think of the architecture this way:
Your Django application
↓
Task API
↓
Queue/backend
↓
Workers
↓
Execution policy
↓
Monitoring / retries / schedulingDjango 6.0 now owns a standard version of the Task API layer.
Celery can cover several layers below and around it.
That means the right design can even involve Django's Tasks API with a production backend that uses another execution system underneath. The abstraction and the worker technology do not have to be the same thing.
The decision table: Django Tasks or Celery?
| Requirement | Django 6 Tasks API | Celery |
|---|---|---|
| Standard Django-native task definition | Excellent | Celery-specific task API |
| Built-in production worker | No | Yes |
| Built-in production broker/queue | No | Uses supported external brokers |
| Simple background email/file processing | Good with a production backend | Good |
| Queue names | Supported by task contract/backend capability | Mature routing/queue controls |
| Task priority | Supported when backend supports it | Supported depending on broker/transport |
| Deferred execution | __INLINE_CODE_0__ when backend supports it | Mature countdown/ETA scheduling |
| Automatic retry/backoff | Backend/application responsibility | Mature __INLINE_CODE_0__ and __INLINE_CODE_1__ |
| Periodic scheduling | Not a complete built-in scheduler | Celery Beat |
| Chains/groups/chords | Not a core Django Tasks feature | Built-in Canvas primitives |
| Distributed worker operations | Backend-dependent | Mature worker model |
| Time/rate limits | Backend-dependent | Mature worker/task controls |
| Monitoring/remote worker control | Backend-dependent | Mature ecosystem and worker controls |
| Portability between task backends | Core design advantage | More Celery-coupled |
| Existing large Celery deployment | No inherent reason to migrate | Keep it unless there is a business reason |
The key phrase in the Django column is often “backend-dependent.”
Django's task contract exposes capabilities such as queue names, priority and deferred execution, but a backend can advertise whether it actually supports them.
When Django Tasks is a good fit
Django's new API is attractive when the problem is fundamentally simple:
HTTP request
→ enqueue independent work
→ run it outside the request
→ record result/status if neededExamples include sending a transactional email, resizing an uploaded image, generating a PDF, calling a non-critical external API, exporting a report, sending a notification, or performing a single independent post-processing step.
For these tasks, adopting a large workflow system can be unnecessary if a reliable production backend meets the application's requirements.
The strongest reason to use Django Tasks is not “fewer dependencies”
It is a stable application-facing contract.
A reusable Django app can define:
from django.tasks import task
@task
def rebuild_search_document(object_id: int):
...without forcing every consuming project to adopt one specific task library at the task-definition layer.
That is strategically useful for the Django ecosystem.
When Celery is still the stronger choice
Celery remains a strong fit when the application needs execution features that go well beyond enqueueing an independent function.
1. Retries are part of the business requirement
Suppose a task calls a flaky external API.
The current Celery task documentation supports explicit retries and automatic retries for selected exceptions:
from requests import RequestException
@app.task(
autoretry_for=(RequestException,),
retry_backoff=True,
max_retries=5,
)
def sync_customer(customer_id):
...Celery supports exponential backoff and jitter controls.
If your production requirements say:
retry 5 times
with exponential backoff
capture retry state
alert after exhaustionthen “the task can be enqueued” is only the beginning.
You need to evaluate whether the chosen Django Tasks backend gives you the reliability semantics you need or whether Celery already solves them cleanly.
2. You need task workflows
Celery's Canvas workflow primitives include __INLINE_CODE_0__, __INLINE_CODE_1__, __INLINE_CODE_2__, __INLINE_CODE_3__ and __INLINE_CODE_4__.
That enables structures such as:
Import file
↓
Parse rows
↓
Process batches in parallel
↓
Wait for all batches
↓
Generate summary
↓
Notify userIf your application needs dependency graphs rather than independent background jobs, Celery's workflow model is materially more mature.
3. You need operational control over workers
Celery's worker documentation includes controls for concurrency, queues, worker inspection, autoscaling, rate limits, time limits and process lifecycle.
That matters when background work is a production subsystem rather than a convenience feature.
For example:
emails queue
→ many lightweight workers
video queue
→ low-concurrency CPU-heavy workers
payments queue
→ isolated high-priority workers
imports queue
→ long-running workersCelery has mature operational patterns for this type of workload.
4. You already operate Celery successfully
If Celery is stable, monitored and understood by the team, Django 6.0 does not create an automatic migration requirement.
Replacing working infrastructure has a cost.
A migration only makes sense if it meaningfully improves developer ergonomics, reliability, operational cost, portability or maintenance burden.
“Django now has __INLINE_CODE_0__” is not, on its own, a business case.
What does __INLINE_CODE_0__ mean in Django Tasks?
Django's task API includes a __INLINE_CODE_0__ attribute, allowing a task to express the earliest time it should run.
For example:
from datetime import timedelta
cleanup_task.using(
run_after=timedelta(minutes=10)
).enqueue(record_id=123)But the Django task reference says this requires a backend whose __INLINE_CODE_0__ capability is __INLINE_CODE_1__.
That distinction is important:
Django defines the contract for deferred execution; the backend determines whether and how it can honour that contract.
Do not assume __INLINE_CODE_0__ is a replacement for a mature recurring scheduler.
What about recurring tasks and cron jobs?
A recurring job such as:
Every day at 02:00
→ reconcile invoicesis different from:
Run this task no earlier than 10 minutes from nowCelery has a dedicated scheduler, Celery Beat, for recurring tasks.
Django Tasks does not by itself provide an equivalent complete periodic scheduling service.
For simple systems, recurring work can be triggered through cron, systemd timers, Kubernetes CronJobs, cloud schedulers or platform-native scheduled jobs, and then enqueue a Django task.
That can be simpler than operating Celery Beat.
But if the application already needs centralised dynamic schedules, admin-managed periodic jobs and distributed Celery workers, keeping scheduling in the same task platform may be preferable.
The transaction race that both approaches must handle
Background tasks often interact with database writes.
This can fail:
with transaction.atomic():
order = Order.objects.create(...)
process_order.enqueue(order_id=order.id)A fast worker may receive the task before the database transaction commits.
The task then queries the order from a separate connection and cannot see it yet.
Django's Tasks documentation explicitly recommends enqueueing through __INLINE_CODE_0__ when a task depends on data created inside a transaction.
For example:
from functools import partial
from django.db import transaction
with transaction.atomic():
order = Order.objects.create(...)
transaction.on_commit(
partial(
process_order.enqueue,
order_id=order.id,
)
)This is not a Django-Tasks-only problem. It is a general background-processing problem.
Moving from Celery to a new API does not remove transaction boundaries.
Pass IDs, not model instances
Django Tasks serialises task arguments through a JSON round trip.
Prefer:
process_order.enqueue(order_id=order.id)over:
process_order.enqueue(order=order)Passing an identifier creates a cleaner distributed-system boundary. The worker can load current database state when it executes.
This reduces stale serialised state, coupling to Python object internals, serializer surprises and oversized task messages.
The same principle is valuable in Celery even though Celery supports a broader serializer ecosystem.
Reliability is a backend question, not a decorator question
This is the biggest architecture trap in the current “Django vs Celery” conversation.
Two projects can contain identical code:
@task
def generate_report(report_id):
...but have completely different production reliability.
Project A:
non-durable queue
one worker
no retry strategy
no dead-letter handling
no monitoringProject B:
durable queue
multiple workers
retry policy
visibility/lease semantics
monitoring
failure recoveryThe decorator looks the same.
The system is not.
When evaluating a production Django Tasks backend, ask:
- Is the queue durable?
- What happens if a worker crashes after claiming a task?
- Is execution at-most-once or at-least-once?
- How are retries implemented?
- Can retries use backoff?
- Are failed tasks retained?
- Can a poison task be quarantined?
- How are workers scaled?
- How are queues isolated?
- What monitoring is available?
- How are results retained and expired?
- Can tasks be deferred?
- How are graceful shutdowns handled?
- What happens during backend outages?
That checklist is more meaningful than asking whether a library has a __INLINE_CODE_0__ decorator.
Background tasks should usually be idempotent
Neither Django Tasks nor Celery can make arbitrary side effects magically exactly-once.
A worker can fail after completing an external action but before marking a task successful.
For example:
Task starts
→ creates ERP record
→ worker crashes
→ task is retried
→ ERP record is created twiceDesign important operations so repeated execution is safe.
Possible strategies include:
- database unique constraints
- external API idempotency keys
- state-machine transitions
- outbox patterns
- business-level “already processed” keys
- reconciliation before repeating side effects
The requirement is independent of which task framework wins the comparison.
A practical architecture for a modest Django SaaS
For a SaaS with emails, imports and report generation:
Django web app
↓
django.tasks API
↓
Production task backend
↓
Durable queue
↓
Worker process
↓
Application services
↓
PostgreSQL / external APIsUse queue separation where useful:
default
emails
imports
reportsKeep task functions thin:
@task(queue_name="reports")
def generate_report(report_id: int):
ReportService.generate(report_id)The task should orchestrate an application service rather than contain hundreds of lines of business logic.
That keeps business logic testable without a queue.
A practical architecture when Celery is justified
For a system with complex background processing:
Django/API
↓
Celery producer
↓
Redis/RabbitMQ or supported broker
↓
Specialised worker queues
↓
Retry / routing / Canvas workflows
↓
Result and monitoring systemsTypical triggers include:
- multi-stage data pipelines
- external APIs requiring controlled retries
- fan-out/fan-in processing
- high task volume
- long-running jobs
- task routing by workload type
- dynamic recurring schedules
- mature Celery monitoring already in place
Celery's complexity has a cost, but complexity can also be a feature when the workload genuinely requires it.
Can you start with Django Tasks and move to Celery later?
Potentially, and this is one of the strongest reasons to adopt Django's standard task API for new simple jobs.
If application code depends primarily on the Django Tasks contract, a production backend can be changed without rewriting every call site.
But portability has limits.
Once code depends on Celery-specific features such as Canvas chains/chords, Celery retries, broker-specific routing, custom Celery Task classes, signals or Beat integration, the application is coupled to those capabilities.
That is not necessarily bad.
A useful architecture separates:
portable simple tasksfrom:
advanced workflow jobsDo not force complex workflow requirements through the lowest common denominator merely to preserve theoretical portability.
Should an existing Celery project migrate to Django Tasks?
Use a migration gate.
Consider migration when
- Celery is used only for simple independent jobs
- the team finds Celery operationally expensive
- a production Django Tasks backend meets reliability requirements
- no Celery-specific workflows are used
- retry requirements are modest or handled elsewhere
- scheduling is simple
- the migration produces a measurable benefit
Keep Celery when
- Celery is stable and already monitored
- chains/groups/chords are heavily used
- retries/backoff are central
- Celery Beat manages important schedules
- queues/routing are operationally important
- worker autoscaling/time limits/rate limits matter
- the team has mature Celery runbooks
- there is no measurable benefit to replacing it
The right answer for a stable production system is often:
Do nothing yet.
A new framework feature should reduce risk or cost before it earns a migration.
What should a new Django 6 project choose?
Use this decision sequence.
Step 1: list the actual jobs
For example:
send signup email
generate invoice PDF
sync CRM
resize images
run nightly reconciliation
process 5 GB import
fan out 20,000 jobs
retry partner API for 24 hoursStep 2: classify every job
Ask about:
- expected duration
- frequency
- acceptable delay
- retry requirement
- idempotency strategy
- scheduling requirement
- concurrency
- priority
- failure visibility
- dependency on other jobs
Step 3: choose the smallest production system that satisfies the requirements
If jobs are independent and simple, Django Tasks plus a reliable backend can be a clean starting point.
If the requirements already describe orchestration, distributed routing, rich retries and worker controls, choosing Celery from day one can be cheaper than re-platforming later.
Step 4: separate business logic from the task framework
Good:
def issue_monthly_statement(account_id: int):
...and a thin Django Tasks or Celery wrapper around it.
This keeps business logic testable without queue infrastructure.
Common mistakes with Django 6 Tasks
Assuming the default backend is asynchronous
__INLINE_CODE_0__ executes immediately. It is not a production background worker.
Assuming Django ships a durable worker
It does not. Production execution requires external backend/infrastructure.
Passing model instances
Pass stable, JSON-safe values such as IDs.
Enqueueing before a transaction commits
Use __INLINE_CODE_0__ when the worker depends on newly committed state.
Choosing a backend only because installation is simple
Evaluate durability, failure recovery and operations.
Migrating from Celery because of a headline
Compare the capabilities your system actually uses.
Common mistakes with Celery
Adding Celery for one trivial task without evaluating simpler infrastructure
Every broker, worker and scheduler adds operational surface area.
Treating retries as permission to write non-idempotent tasks
Retries make idempotency more important, not less.
Mixing every workload into one queue
Heavy imports can starve urgent jobs when routing/concurrency is not designed.
Hiding business logic inside task classes
Keep application services independently testable.
Operating Celery without monitoring
Background work is production work. Failures need visibility.
A concise recommendation matrix
| Your situation | Recommended starting point |
|---|---|
| New Django 6 app with a few independent jobs | Django Tasks + production-ready backend |
| Transactional email, small reports, simple post-processing | Django Tasks + suitable backend |
| Need retries/backoff as a core feature | Celery or a backend with equally explicit semantics |
| Need chains/groups/chords | Celery |
| Need mature periodic scheduling in the task stack | Celery + Beat |
| Need complex routing and worker operations | Celery |
| Existing healthy Celery system | Keep Celery unless migration has measurable value |
| Reusable Django package exposing simple background jobs | Django Tasks API is attractive |
| Unsure about future scale | Start from requirements and keep business logic framework-neutral |
Neither “built in” nor “battle-tested” should decide the architecture by itself.
FAQs
Does Django 6.0 replace Celery?
No. Django 6.0 introduces a standard Tasks API, but Django's documentation says it does not provide a production worker mechanism. Celery remains a mature distributed task execution system with retries, workers, scheduling, routing and workflow primitives.
Can Django 6.0 run background tasks without Celery?
Yes, but production execution still needs an external production-capable task backend and worker infrastructure. Django's built-in __INLINE_CODE_0__ and __INLINE_CODE_1__ are intended for development and testing rather than production asynchronous processing.
Does Django Tasks support retries?
Django defines the common task/backend contract, but retry behaviour is not a built-in Celery-style retry system in the core API. Evaluate the production backend's retry and failure semantics. Celery provides explicit __INLINE_CODE_0__ and __INLINE_CODE_1__ features with backoff controls.
Does Django Tasks support scheduled jobs?
The task contract includes __INLINE_CODE_0__ for deferred execution when the backend supports it. That is not the same as a complete recurring scheduler such as Celery Beat. Recurring jobs can also be triggered by cron, Kubernetes CronJobs or cloud schedulers.
Should I migrate an existing Celery project to Django Tasks?
Not automatically. If Celery is stable and the project uses retries, routing, Beat or Canvas workflows, migration may remove useful capabilities. Consider migration only when a simpler production backend satisfies actual requirements and produces measurable operational or development benefits.
What is the biggest advantage of Django's built-in Tasks framework?
It gives Django applications and reusable packages a standard task-definition and enqueueing contract. That can reduce application-level coupling to a particular task implementation for simple background jobs.
Conclusion
Django 6.0 changes background-task architecture, but not in the way the “Celery killer” headline suggests.
Django now gives the ecosystem a common contract:
define
→ validate
→ enqueue
→ trackProduction infrastructure still has to provide:
durability
→ workers
→ execution
→ retries
→ failure recovery
→ operationsFor simple independent jobs, Django Tasks plus a reliable production backend can be a clean and intentionally smaller system.
For applications that need rich retries, recurring schedules, task graphs, routing and mature worker operations, Celery remains a strong choice.
The best architecture is the smallest system that can meet your background-work reliability requirements without making future operations fragile.
If you are designing a Django SaaS or modernising an existing backend, Softotic's custom software development service can help structure the application and worker architecture, while web application development can cover the Django/API and user-facing application layer.