Exploring Django Tasks
Recap on where things stand
With the release of Django 6 and above, a new task framework has been introduced. Despite what some articles might suggest, this framework is not immediately beneficial for production environments and does not represent an instant "game changer." Instead, it marks a positive step towards creating a back-end agnostic interface for queuing tasks.
Currently, the framework only includes a dummy and immediate back end. As a result, it does not serve as a replacement for established solutions like Celery.
If all you want is a step-by-step guide to using named queues, skip down to “the quick guide to custom queues.” Everything else is just documenting my exploration in the hope it helps someone else get there faster.
Django Tasks and django-tasks
If you google for information, you’ll find the Django documentation on tasks as well as many tutorials that refer to the django-tasks package.
The django-tasks package is an implementation (“backport” it is described as) that many tutorials refer to as a way to use a database back end and a way to run a task completion process. I can’t comment on whether this is “production ready”. The tutorials suggest “manage.py db_worker" as the way to run this, which feels like suggesting manage.py runserver is acceptable for production. But that’s for later exploration. At the moment, the docs say:
Prior to
0.12.0,django-tasks-dbanddjango-tasks-rqwere also included to provide database and RQ based backends.
So I’m not sure most of the search results for how to use Django tasks to run background tasks are valid at this point.
Everything in this article, though, is based on using the task framework built in to Django>=6 but seems to be identical if you’re using the django-tasks package. And the README.md for django-tasks is really useful for understanding the basics.
Queues
I’m trying out the new task framework in a hobby project. I decided I would make use of the queuing system. At the moment I only have one kind of task, but I may add other tasks later just because it will be there and available.
There’s a note that if you change the queues enable you would get an InvalidTaskError exception. This caught me out because the phrasing is:
Enqueueing tasks to an unknown queue name raises
InvalidTaskError.
My brain took that to mean that at the point of enqueueing, I would get an error. This is not the case.
So I added the section about queue names from the guide. At first this is not added to the TASKS settings dictionary. It also explains that in order "To disable queue name validation, set QUEUES to []." See below for more info on this below - I did some experimenting to ensure my understanding was correct before moving on, and it might be useful for others to see how that went.
django.tasks.exceptions.InvalidTask: Queue 'default' is not valid for backend.
This is not the same as the InvalidTaskError and I was a bit confused. The issue was, it turns out, that the decorator for the task function needs to specify the queue. As the file containing the task function is evaluated, the exception fires. I’m assuming the InvalidTaskError is somehow triggered if you enqueue some other way.
The quick guide to custom queues
Everything else in this post is about my exploration - realisations, mistakes and thoughts included. But here is the quick guide to using custom queues with the task framework:
Add the queue name to the
QUEUESlist in settings.Decide if you want any existing task definitions to continue using the default queue. If so, make sure the
QUEUESlist also contains”default”.Add the
queue_nameto the task decorator
Here are the key pieces of code:
# In settings.py
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": [
"my_queue",
#Uncomment below to keep existing tasks using the default queue
#"default",
],
}
}
# In tasks.py
from django.tasks import task
@task(queue_name="my_queue")
def some_task(id):
"""Task for doing something with an object with given id"""
pass
Absence of QUEUES is not the same as empty QUEUES
You can skip this section if you don’t care about how I proved to myself that I understood how queue validation worked.
From the django-tasks README.md (at the time of writing: 09-02-2026),
"To disable queue name validation, set QUEUES to []."
You might assume this means that without an explicit QUEUES list in the TASKS settings dict, there is no queue name validation but that’s not the case.
After I’d realised my mistake with the queue_name parameter, I experimented.
I figured there were multiple states of the QUEUES list that mattered. Not having one (1), having one with just the new queue name (2), having one with just “default” (3), having both (4) and having it as an empty list (5).
# 1
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
}
}
# 2
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": [
"my_queue",
],
}
}
# 3
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": [
"default",
],
}
}
# 4
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": [
"default",
"my_queue",
],
}
}
# 5
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": [],
}
}
Then the task decorator could either not specify a queue (1), specify default (2) or specify a new queue name (3).
# 1
@task
def my_task(id):
pass
# 2
@task(queue_name="default")
def my_task(id):
pass
# 3
@task(queue_name="my_queue")
def my_task(id):
pass
As it happens, it seems 1 and 2 are identical, which makes sense. By default, the queue is “default”.
| Default queue in decorator | Custom queue name in decorator | |
| No QUEUES setting | ️✅ | ❌ |
| Custom queue only | ❌ | ️✅ |
| Default queue only | ️✅ | ❌ |
| Default and custom queues | ️✅ | ️✅ |
| Empty QUEUE list | ️✅ | ️✅ |
Where does this leave us?
For my own exploration, I now have something that works with the new framework and I'm hopeful that with a combination of django-tasks and django-tasks-db I will have something that functions for background tasks. When I get to that point, I will have to dig into what might be safest for running in production. Can I really just use db_runner? Or will I have to roll my own task process? Or maybe there are other packages I can use to handle the tasks from the DB?
When I get time, I'll find out and post.



