Django Background Tasks: Choose a Queue
Compare Django 6 Tasks, Django Q2, and Celery, then choose a background-job setup using workload, reliability, and operations criteria.
For most small Django SaaS applications, Django Q2 is a sensible default when you want a Django-native queue, scheduler, worker, and admin experience. Choose Celery when you need richer workflow primitives, broader routing and broker options, or an ecosystem your team already knows how to operate. Use Django 6's Tasks framework when you want Django to define the task interface, but remember that it still needs an external production backend and worker.
The right choice starts with the work and its failure modes—not with the most popular package.
The short version
| Choose | When it fits | Watch for |
|---|---|---|
| Django Tasks | You want a standard Django task API and have a compatible production backend | Django does not ship the production worker; built-in backends are for development and testing |
| Django Q2 | You want a compact Django-first stack for application jobs, schedules, and visible task administration | Broker guarantees differ; the Redis broker does not support receipts |
| Celery | You need mature distributed-task features, workflow composition, routing, or established operational tooling | More concepts and operational surface than many small applications need |
If your background work is limited to emails, webhook side effects, reports, file generation, and a few scheduled jobs, start by evaluating Q2. If tasks branch into chains, groups, and fan-out/fan-in workflows, or must run across specialized queues at scale, evaluate Celery. If you are adopting Django 6, treat its Tasks framework as an application-facing contract rather than a replacement for queue infrastructure.
If you are still deciding whether the work belongs in the request at all, use the Django async views vs background tasks guide. It separates concurrent request I/O from durable worker-owned jobs before you choose a queue.
A background task system has five parts
“Run this later” sounds like one feature. In production it is a system with separate responsibilities:
- Task definition: the function, its allowed arguments, and its return value.
- Task backend: the interface that accepts work and tracks supported features.
- Broker or queue: the transport and storage used between the web process and workers.
- Worker: the process that claims and executes work.
- Application state: the durable record a user or another system can
inspect, such as
queued,processing,ready, orfailed.
Package comparisons become confusing when these layers are collapsed. Django 6's Tasks framework standardizes the first two from application code's point of view, but it does not include a production worker. Django Q2 and Celery provide queue-and-worker systems with different operational and delivery tradeoffs.
Application state deserves its own layer. A task queue may know that message
abc123 failed, but your user needs to know that invoice export 42 failed and
what to do next. A domain record is also a more stable integration point than
a broker-specific task result.
Define the workload contract before choosing a queue
List the jobs you expect to run and answer these questions:
- Latency: must the job start in one second, one minute, or eventually?
- Duration: is normal execution 200 milliseconds, 30 seconds, or an hour?
- Volume: do you enqueue ten jobs a day or thousands per minute?
- Failure behavior: can the job be retried, and what happens if it runs twice?
- Ordering: can two jobs for the same record run concurrently?
- Scheduling: do you need recurring schedules or only ad hoc work?
- Composition: must jobs form chains, parallel groups, or callbacks?
- Isolation: does one workload need its own worker pool or deployment?
- Visibility: what must a user, support engineer, or agent be able to inspect?
- Operations: what broker, monitoring, and deployment experience does the team already have?
This turns “Q2 or Celery?” into a testable decision. A team generating a few hundred ZIP archives per day has a different contract from a system processing millions of independent events or coordinating a multi-stage media pipeline.
If the workload is recurring, use the Django scheduled-tasks decision guide to define one scheduler authority, intended run slots, missed-run behavior, overlap, idempotency, and completion evidence. Those policies are separate from the queue choice.
It also exposes cases where no queue is necessary. If a result is required to complete the current request and finishes quickly, synchronous execution can be easier to reason about. Moving work to a queue adds eventual consistency, worker deployment, retries, and another failure boundary. Use that complexity when it protects request latency or improves reliability.
Option 1: Django 6's Tasks framework
Django 6.0 introduced the Tasks framework as a standard way to define and enqueue background work. A task can specify a backend, queue name, priority, and delayed execution when the configured backend supports those features. Arguments are validated and must survive a JSON round trip.
That standard application interface is useful. Business code can express “enqueue this task” through Django rather than importing a queue package throughout the codebase. Backend capabilities are explicit, and task definitions live in a familiar Django module.
The important limitation is equally explicit in Django's Tasks documentation: Django handles task definition and queuing, but actual execution requires external infrastructure. The built-in Immediate and Dummy backends are useful for development and tests, not production work. You still need to choose, configure, deploy, and monitor a production backend and worker.
Choose the Django Tasks API when:
- a standard Django-level task contract matters to the codebase;
- a suitable backend supports the capabilities you need;
- you are comfortable operating that backend's workers;
- you accept that some advanced behavior remains backend-specific.
Do not choose it because you expect manage.py runserver to execute durable
jobs by itself. The framework reduces application coupling; it does not remove
the distributed system.
Option 2: Django Q2
Django Q2 provides a
Django-native task queue, scheduler, worker cluster, result models, and admin
integration. Application code queues a function with async_task, while
workers run through python manage.py qcluster. It supports multiple brokers,
including Redis and Django's ORM, and a synchronous mode that is useful in
tests.
Q2 is a good fit when the work belongs closely to the Django application and the team wants fewer moving parts in application code. Common examples include:
- sending transactional email outside the request;
- processing webhook side effects;
- generating reports or downloadable artifacts;
- calling non-critical third-party integrations;
- running scheduled cleanup and maintenance;
- recording analytics that should not delay a response.
The Django admin and schedule model are practical advantages for a small team. You can inspect successes and failures alongside application data without building a separate task console on day one. Q2 also supports named queues and multiple clusters when workloads need some separation.
Understand the broker guarantee
“Uses Redis” does not fully describe delivery behavior. Q2's broker documentation says its Redis broker is atomic but does not support receipts. If a cluster is lost catastrophically while a task is in progress, that task cannot be offered to another worker through receipt-based redelivery. This is different from an ordinary task exception, which Q2 can record as a failure.
Other Q2 brokers support receipts and redelivery. When using those brokers,
the Q2 configuration
guide warns that
retry must be greater than timeout; otherwise a broker can offer a task
again while the first worker is still running it. The result can be concurrent
duplicate execution.
This does not make Q2 unsuitable for production. It means you must match the broker to the failure contract, make retryable tasks safe to repeat, and keep user-visible state outside the queue.
Choose Q2 when:
- workloads are bounded and understandable;
- Django-native configuration and administration are valuable;
- schedules and straightforward queues cover the job graph;
- your broker's delivery tradeoffs are acceptable;
- the team will run and monitor a dedicated worker process.
Option 3: Celery
Celery's Django integration provides task autodiscovery, Django settings integration, and dedicated worker processes. Celery is a mature distributed-task system with configurable brokers, result backends, routing, retries, time limits, and extensive worker controls.
Its clearest advantage appears when background work becomes a platform of its own. Celery Canvas supports chains, parallel groups, and chords that run a callback after a group. Separate queues can route CPU-heavy work, external API calls, and latency-sensitive jobs to different workers. The ecosystem also includes widely used monitoring tools and established operating patterns.
Those capabilities come with more choices. A team must understand broker and result-backend behavior, worker concurrency, acknowledgement timing, routing, retries, and deployment. Celery's task guide emphasizes idempotency because late acknowledgement and worker-loss redelivery can cause a task to execute more than once.
Choose Celery when:
- workflow composition is a core requirement;
- tasks need sophisticated routing or dedicated worker pools;
- the team already has Celery expertise and observability;
- workload scale or isolation justifies the operational surface;
- the broader ecosystem solves concrete requirements you have now.
Avoid choosing Celery only because it is the familiar answer to “background tasks in Django.” For a small application, unused flexibility is still complexity that must be configured, upgraded, and debugged.
Django Tasks vs Q2 vs Celery
| Criterion | Django Tasks | Django Q2 | Celery |
|---|---|---|---|
| Primary role | Standard task definition and enqueue API | Django-native queue and worker system | Distributed task queue and workflow system |
| Production worker included | No | Yes | Yes |
| Development/test mode | Immediate and Dummy backends | Synchronous mode; ORM broker is also available | Eager execution settings |
| Scheduling | Backend-dependent | Built-in schedule model | Celery Beat |
| Workflow composition | Backend-dependent | Straightforward task and hook patterns | Chains, groups, chords, callbacks |
| Routing/isolation | Backend-dependent | Named queues and clusters | Mature routing and multiple queues |
| Django admin visibility | Backend-dependent | Built-in task and schedule models | Usually separate monitoring tooling |
| Best fit | Stable Django-facing API | Bounded Django application jobs | Complex or distributed job platforms |
The table is a starting point, not a benchmark. Delivery guarantees depend on the selected backend or broker and its configuration. Throughput depends on the task, worker model, database access, network calls, and deployment—not the package name alone.
How Djass structures background generation with Q2
Djass generates Django SaaS repositories in the background. The web request should return quickly, while Cookiecutter rendering, manifest creation, ZIP packaging, checksum calculation, and file storage happen in a worker.
We use a record-first queue contract:
- The request validates input and creates a durable
Projectrow with a normalized payload andqueuedstatus. - Request code enqueues
generate_project_artifactwith the project ID—not a model instance or a large serialized payload. - A Q2 worker loads the current record and moves it to
generating. - On success, the worker stores the artifact, its size, and SHA-256 checksum,
then marks the project
ready. - On failure, it persists an actionable error and marks the project
failed. - The dashboard and API expose status, and an explicit retry path can enqueue failed generation again.
You can inspect the full project generation pipeline and the focused Django Q2 background-job setup.
This design makes the application record the source of truth for user-visible state. If a worker fails, support does not need a broker message ID to explain what the user sees. Passing the project ID keeps task input small and lets the worker read current state. Persisting failures creates a recovery path instead of leaving a spinner with no explanation.
Our queue settings make the workload assumptions visible: four workers, a
3,600-second task timeout, a 4,800-second retry interval, and two attempts.
The repository provides a synchronous, ORM-backed Q2 settings module for
targeted tests, while the default test suite stubs queue dispatch where
appropriate. Deployed workers run qcluster.
These choices fit an artifact-generation workload, but they are not universal defaults. The long timeout would be inappropriate for a short email task. Four workers may be too many for a memory-heavy renderer or too few for a high-volume webhook system. And because Q2's Redis broker has no receipts, the durable status and retry path improve diagnosis and user recovery without turning Redis into a receipt-capable broker.
What would make us reconsider Q2?
We would revisit the queue if the workload required:
- complex chains, parallel groups, and join callbacks as a core abstraction;
- strong receipt-based redelivery without changing the current broker setup;
- many independently scaled queues with specialized worker deployments;
- organization-wide Celery operations and monitoring already in place;
- a Django Tasks backend that met the same workload and operating needs with a useful standard application contract.
That review trigger is more useful than loyalty to a package. Infrastructure should change when the workload contract changes.
Reliability rules for every background-task option
Pass identifiers and primitives
Queue record IDs, strings, numbers, and small dictionaries. Do not pass live model objects, request objects, open files, or hidden process state. Django Tasks explicitly requires JSON-serializable arguments, and the same discipline makes Q2 and Celery tasks easier to retry and migrate.
Design for repeated execution
A worker can fail after an external side effect but before recording success. A receipt-capable broker may then redeliver the task. Use idempotency keys, unique database constraints, state checks, or an explicit “already completed” guard where repetition would send two emails, charge twice, or create two artifacts.
Separate timeout from redelivery
A runtime limit answers “how long may this worker execute?” A retry or visibility interval answers “when may another worker receive this job?” They must be configured together. Q2 documents the risk directly: with receipt-capable brokers, a retry interval shorter than the task timeout can produce concurrent duplicates.
Persist state users care about
Store lifecycle status and actionable failure details on the relevant domain record. Broker results are useful operational evidence, but they should not be the only explanation available to a user or API client.
Monitor the worker, not just the web app
A healthy Django web process can keep accepting work while every worker is down. Monitor queue depth, oldest-job age, worker heartbeat or process health, failure rate, task duration, and repeated retries. Alert on a growing backlog before users report it.
Test the failure path
Test task success, known failure, duplicate execution, missing records, and retry behavior. Synchronous test modes make assertions convenient, but also run an integration test with the real broker and worker before relying on delivery behavior in production.
A practical decision checklist
Before adopting or replacing a queue, write down:
- the three most important jobs and their normal/max duration;
- required start latency and daily/peak volume;
- acceptable delivery guarantee and duplicate-execution risk;
- scheduling and workflow-composition needs;
- required queues, routing, and workload isolation;
- broker and worker infrastructure the team can operate;
- user-visible status and recovery behavior;
- monitoring, alerting, and support ownership;
- a specific condition that would trigger reevaluation.
Then build one representative job end to end. Include the worker deployment, failure persistence, retry safety, monitoring, and recovery—not only the enqueue call. The smallest useful proof is a job you can intentionally break and confidently recover.
Djass uses Q2 because it fits its current Django-first generation workload and keeps the queue design inspectable. The available generator modules show what is included in generated projects, and Djass pricing shows the current cost if you want that foundation generated for you.
The final choice is less about Django Q2 versus Celery as brands. It is about whether your task system can execute the work, fail visibly, repeat safely, and be operated by the team that owns it.