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.

Why a worker drops a taskWeb app sendsname orders.tasks.xBroker queuesstores only the nameWorker looks upits own registryNot foundmessage discardedThe worker only knows tasks from modules it imported at startup, not what the web app knows.
  1. Run celery -A config inspect registered and check the task name is in the list.
  2. If it is missing, make sure your Celery app calls app.autodiscover_tasks() and the app is listed in INSTALLED_APPS.
  3. Start the worker with the same -A app and the same DJANGO_SETTINGS_MODULE as the web process.
  4. Do not mix relative and absolute imports of task modules; give tasks an explicit name= if they are renamed or moved.
  5. 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"].

what the mismatch usually isWhat you seeCause and fixmodule missing from inspect registerednot in tasks.py or app not inINSTALLED_APPS; autodiscoveror CELERY_IMPORTSname differs only by prefixrelative import or sys.pathdifference; import by fullpath or set name=old name after a refactorstale messages from the previousrelease; keep an alias until drainedworks on one worker, fails on anotherworkers run different images orsettings; redeploy all of themThe registry is built at worker startup, so every cause is an import differencedjango-deployment.com
Match the symptom to the cause, then restart every worker so they all build the same registry.

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.

renaming a task without dropping messages1Add the new task namekeep the old one as an alias2Deploy workers firstthey accept both names3Deploy web and Beatthey send only the new name4Wait for old messages to draincheck queue length5Remove the aliasnext releaseWorkers must understand a name before anything is allowed to send itdjango-deployment.com
Deploy consumers before producers so no message arrives with a name the worker cannot resolve.

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.py raises an exception, autodiscovery can skip it silently. Run python -c "import orders.tasks" with production settings to surface the traceback.

Next steps

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=.

Keep going