CircleCI Changelog
CircleCI Consolidates Breaking Changes: What Developers Need to Update Before July 17

Django Gets a Built-in Task Queue: What Developers Need to Know About Persistence

July 22, 2026

Last updated: July 19, 2026

YouTube thumbnail artwork for Python Bytes Podcast Episode #488, 'tau - it's 2pi and it writes code', featuring the show branding and episode title.
YouTube thumbnail artwork for Python Bytes Podcast Episode #488, ‘tau – it’s 2pi and it writes code’, featuring the show branding and episode title.

Image: Python Bytes Podcast

For roughly twenty years, adding background tasks to a Django project meant reaching for a third-party library. Celery, RQ, Huey – pick your poison, provision your message broker, write your config, and hope future-you remembered why you chose Redis over RabbitMQ. Django 6.0 changes that. The framework now ships a Django async task queue API out of the box, and if you’re building anything beyond a toy project, there’s one thing you need to understand before you push to production.

What django.tasks Actually Is (And What It Isn’t)

YouTube thumbnail artwork for Python Bytes Podcast Episode #488, 'tau - it's 2pi and it writes code', featuring the show branding and episode title.
YouTube thumbnail artwork for Python Bytes Podcast Episode #488, ‘tau – it’s 2pi and it writes code’, featuring the show branding and episode title.

Image: Python Bytes Podcast

Here’s the critical distinction that will save you a headache: django.tasks is an API, not a worker. Think of it like a staffing agency that handles all the paperwork – vetting candidates, managing contracts, tracking results – but doesn’t actually send anyone to do the job. You still need to hire the worker yourself.

Concretely, Django handles task definition, validation, queuing, and result storage. What executes your tasks depends entirely on which backend you configure. That design decision is deliberate and actually quite sensible: it means the core Django team can own the interface contract without having to maintain a distributed task execution engine.

This all started with Jake Howard’s DEP 14 (a Django Enhancement Proposal – the framework’s equivalent of a Python PEP), accepted in May 2024. The fact it made it from accepted proposal to shipping in 6.0 in roughly two years says something about how much appetite there was for first-party async support. No more bolting on third-party libraries for something as fundamental as “run this function later.”

The Default Backend Will Trap You

The default backend is called ImmediateBackend, and the hosts of Python Bytes episode #488 were blunt about it: it “traps people.” Here’s why.

ImmediateBackend runs tasks inline on the request thread. So when a user submits a signup form that triggers a welcome email, that email sends synchronously before the HTTP response returns. Not background at all – just a regular function call with extra ceremony around it. You might think this is fine for development and harmless for small traffic. Actually it’s a footgun: your code works perfectly in dev, you ship to production, and suddenly a slow email task is blocking every signup response. Your users sit staring at a loading spinner while your mail server does its thing.

Before django.tasks, you’d at least have to consciously install Celery and know you were setting up a real background worker. With ImmediateBackend, you can write code that looks like async task code, test it locally, and never realise it isn’t truly async until your users notice.

The fix is straightforward: swap the backend. A community-maintained ThreadPoolExecutor backend gives you real background threads with zero infrastructure – no Redis, no Celery, no database required. For CPU-bound work there’s a ProcessPoolBackend too. These live in the lincolnloop django-tasks-scheduler project and are worth evaluating before you default to wiring up a full Celery stack.

Three Jobs, Three Different Answers

Walk through three jobs that show up in almost every Django project and the trade-offs snap into focus.

Welcome email on signup. The thread pool backend is genuinely excellent here. The task fires and forgets, response returns immediately, and if the task drops on a process restart before it runs – a restart that almost certainly coincides with a deploy – your mail provider’s retry logic or a simple re-trigger handles it. Low stakes, high convenience. This is what the thread pool was made for.

Image thumbnail on upload. Similar story, with one caveat: if your thumbnail job is CPU-bound (resizing, re-encoding), use ProcessPoolBackend rather than the thread pool. Python’s Global Interpreter Lock (GIL) – the mechanism that prevents multiple threads from executing Python bytecode simultaneously – means CPU-heavy work in threads doesn’t actually parallelise. Processes do. Switch the backend in your config, your task code doesn’t change at all.

Payment webhook. Different beast entirely. A lost payment event isn’t a minor inconvenience – it’s a support ticket, a failed order, possibly a compliance issue. You need at-least-once delivery: a guarantee that even if your process crashes mid-task, the job will be retried. That requires a durable queue backed by a database or message broker, because those systems write the task to persistent storage before acknowledging it. Thread pools and process pools keep tasks only in memory. If the process dies, the task dies with it.

This is where persistence means something operationally concrete. A durable backend lets you set retry counts, enforce idempotency (making a task safe to run twice without doubling the side-effect – critical for anything that touches money or external APIs), and inspect failed jobs after the fact. None of that is available with the in-process backends. You’re not missing a nice-to-have; you’re missing a safety net.

The Infrastructure Trade-off Most Articles Skip

Here’s the nuance that gets glossed over in announcement posts: thread-based and process-based backends are perfect for many workloads, but they don’t survive process restarts. If your web server goes down mid-task, that task is gone.

This is actually the same trade-off you face when choosing between different full-stack web frameworks – convenience versus operational guarantees. A thread pool backend is like keeping your important papers in a desk drawer versus a fireproof safe. Most days the drawer is fine. The day there’s a fire, it isn’t.

For greenfield projects doing things like sending welcome emails or processing image thumbnails, the thread pool backend is genuinely excellent – fast to set up, no infrastructure to maintain, and the django.tasks API means you can swap in a more robust backend later without rewriting your task definitions. That’s the real win here: the interface is stable even if the backend isn’t permanent. Compare that to migrating from Celery to RQ, which often means rewriting task decorators, retry logic, and monitoring hooks throughout a codebase.

Myth-Busting

“django.tasks IS the background worker.” No – it’s the interface that sits in front of a worker. The API standardises how you define, queue, and inspect tasks. The execution engine is the backend you configure. Django ships the contract; you choose the implementation.

“ImmediateBackend is fine for low-traffic production.” It isn’t. At any traffic level, ImmediateBackend runs tasks synchronously on the request thread. A slow task blocks a response. The risk isn’t load – it’s latency. Even on a quiet site, a task that hits an external API adds that API’s response time to every request that triggers it.

“Thread pool backend means I never need Celery again.” Sometimes true. For workloads where losing an in-flight task on process restart is acceptable – thumbnails, non-critical emails, analytics events – the thread pool is sufficient and far simpler. For anything requiring at-least-once delivery, retries on failure, or idempotency guarantees, you still need a persistent backend. The difference is that now you connect it to a standardised Django API rather than adopting an entirely separate ecosystem.

“Switching backends later will break my task code.” It won’t, and this is deliberate. Because django.tasks separates the API from the execution layer, migrating from thread pool to a database-backed queue means changing your backend config, not your task definitions. That portability is the actual long-term value here.

So: does Django’s built-in task queue replace Celery? For simple workloads, yes, absolutely. For anything requiring durable, at-least-once delivery across process restarts, you still need a persistent backend – but now you can wire one up to a standardised Django API rather than adopting an entirely separate ecosystem. That’s not nothing. That’s twenty years of friction, quietly removed.

Source: https://pythonbytes.fm/episodes/show/488/tau-its-2pi-and-it-writes-code

This article was researched and written with AI assistance, then reviewed for accuracy and quality. Nia Campbell uses AI tools to help produce content faster while maintaining editorial standards.

Nia Campbell

Nia Campbell writes practical web development guides and incident explainers, translating deployment and tooling changes into step‑by‑step actions for UK teams and business owners.

Need help with your web project?

From one-day launches to full-scale builds, DRS Web Development delivers modern, fast websites.

Get in touch

    Comments are closed.