Fix Celery "Received unregistered task" in Django
Fix the Celery KeyError and Received unregistered task of type message in Django: make workers import the same task modules as the web app, align task names, and stop stale queued messages from old releases.
Problem statement
A Django view queues a task, the broker accepts it, and the worker log shows:
[ERROR/MainProcess] Received unregistered task of type 'orders.tasks.send_receipt'.
The message has been ignored and discarded.
Did you remember to import the module containing this task?
Or maybe you're using relative imports?
KeyError: 'orders.tasks.send_receipt'
The task never runs and its result stays PENDING. The web process clearly knows the task, because it sent it. The worker does not. Every Celery message carries only a task name; the worker looks that name up in its own registry, which is filled by importing task modules when the worker starts. If the worker imported a different set of modules, or the name was spelled differently when the task was sent, the lookup fails and the message is thrown away.
- Run
celery -A config inspect registeredand check the task name is in the list. - If it is missing, make sure your Celery app calls
app.autodiscover_tasks()and the app is listed inINSTALLED_APPS. - Start the worker with the same
-Aapp and the sameDJANGO_SETTINGS_MODULEas the web process. - Do not mix relative and absolute imports of task modules; give tasks an explicit
name=if they are renamed or moved. - After a rename, keep an alias for the old name until queued messages drain, then restart every worker.
Step-by-step solution
1. Compare the registry with the name being sent
List what a running worker has registered:
celery -A config inspect registered
Each worker prints its tasks. Compare them with the name in the error. Typical mismatches are a module that is not in the list at all, a task registered as tasks.send_receipt while the sender uses orders.tasks.send_receipt, or an old name such as orders.tasks.email_receipt that no longer exists in the code.
If inspect returns nothing, the worker is not connected to the same broker as your shell; fix that first with Debug Celery Not Running in Django Production.
2. Make the worker import every task module
The standard Django setup has a celery.py next to settings.py:
# config/celery.py
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
And config/__init__.py imports it so Django loads the app:
from .celery import app as celery_app
__all__ = ("celery_app",)
autodiscover_tasks() imports tasks.py from every app in INSTALLED_APPS. Tasks defined anywhere else, such as orders/jobs.py or a tasks/ package whose __init__.py does not import its submodules, are invisible to the worker. Either move them into tasks.py, import them from it, or list the modules explicitly with CELERY_IMPORTS = ["orders.jobs"].
3. Start the worker with the same app and settings
The worker command must point at the same Celery app the web process uses and load the same settings:
DJANGO_SETTINGS_MODULE=config.settings.production \
celery -A config worker -l info
In systemd units, Docker Compose services, and Kubernetes manifests, check that the worker's environment includes the same settings variable as the web service. A worker that falls back to a development settings module may have a shorter INSTALLED_APPS, so some apps are never discovered. Running from a different working directory can also change how modules resolve; set WorkingDirectory= in systemd or working_dir: in Compose to the project root.
4. Fix name mismatches from imports
Celery derives a task's name from the module it was imported through. If one place imports from tasks import send_receipt (because a directory is on sys.path) and another uses orders.tasks, you get two names for one function. Use absolute imports everywhere, and pin names on tasks that are part of a public contract:
from celery import shared_task
@shared_task(name="orders.send_receipt")
def send_receipt(order_id):
...
An explicit name also survives moving the function to another module.
5. Handle renamed tasks and stale messages
If you renamed or deleted a task, messages queued by the old release still carry the old name. Keep a thin alias for one release:
@shared_task(name="orders.tasks.email_receipt")
def email_receipt_legacy(order_id):
return send_receipt(order_id)
Remove the alias once the queue holds no old messages. Also check scheduled tasks in django_celery_beat: a periodic task row that points at a removed name produces this error every time it fires. Update or disable it in the admin.
Verification check:
celery -A config inspect registered | grep send_receipt
python manage.py shell -c "from orders.tasks import send_receipt; print(send_receipt.delay(1).get(timeout=30))"
The task should appear in the registry on every worker and the shell call should return a result.
Explanation
Celery decouples sender and worker completely: a message is a JSON body with a task name, arguments, and headers. Nothing in it says where the code lives. Each worker process builds a dictionary from names to functions when it boots, by importing the Celery app and whatever modules autodiscovery or CELERY_IMPORTS pull in. "Unregistered" therefore always means the worker's imports differ from the sender's, either because a module was not imported, the same function was registered under a different name, or the name belongs to code that no longer exists.
Edge cases / notes
- Different images for web and worker. If the worker image is built from an older commit, it lacks new tasks. Build one image and run it with different commands.
- Multiple Celery apps. Projects with two Celery apps sometimes route a task to a queue consumed by the other app's workers. Use explicit queue routing so each app's tasks reach only its own workers.
- Discarded, not retried. Celery acknowledges and drops unregistered messages. They cannot be recovered after the fact, so fix registration before replaying any work.
- Early import errors. If importing a
tasks.pyraises an exception, autodiscovery can skip it silently. Runpython -c "import orders.tasks"with production settings to surface the traceback.
Next steps
- If tasks are not even reaching workers, start with Fix Celery Tasks Stuck in PENDING.
- Old code after a deploy has its own fix: Fix Celery Tasks Running Old Code After Deploy.
- Review the baseline in Configure Redis and Celery for Django in Production.
FAQ
Why does the web app know the task but the worker does not?
The web app registers tasks when its code imports them. The worker registers only what its startup imports pull in. If a module is outside autodiscovery, only the sender knows it.
Does restarting the worker fix it?
Only if the worker was running old code. If the module is not autodiscovered, a restart rebuilds the same incomplete registry.
Can I recover the discarded messages?
No. Celery acknowledges them before discarding. Re-queue the work from your own data once registration is fixed.
Should every task have an explicit name?
Not necessarily, but tasks that are called across services, scheduled in Beat, or likely to move between modules benefit from a stable name=.