Why would a Celery worker log 'Received unregistered task of type' for an invoice task, and how does Celery name tasks by default?
answer
- the message carries a name, not code
- module path plus function name
- the worker's own registry
- an explicit name pins it
basics
~20 sA Celery message carries only the task's name, looked up in the worker's own registry. Names default to module path plus function name, so a worker that never imported the module, or imported it under another path, rejects it as unregistered.
solid answer
~40 s`@app.task` names a task from where it is defined: `render_invoice_pdf` in module `shop.billing.tasks` becomes `shop.billing.tasks.render_invoice_pdf`. The producer publishes that string, not code. The worker has its own registry, filled by the modules it imported at start-up, so the error means that name is missing there: the module is not in the worker's `include` or `imports`, it was imported under a different module path, the task was renamed or moved while old messages sat in the queue, or producer and worker run different code. The worker logs the error and rejects the message without requeueing it, so that invoice is lost. I diagnose with the `[tasks]` list in the worker banner or `celery -A shop inspect registered`, and pin important tasks with `name=`.
code
python · 14 linesfrom celery import Celery
app = Celery('shop', broker='redis://localhost:6379/0',
include=['shop.billing.tasks'])
# shop/billing/tasks.py
@app.task(name='billing.render_invoice_pdf')
def render_invoice_pdf(order_id):
...
print(render_invoice_pdf.name) # billing.render_invoice_pdf
# A producer without the task code sends by name:
app.send_task('billing.render_invoice_pdf', args=(42,))go deeper
Recall that messages carry a task name, that the default is module path plus function name, and that the worker must import the task module.
Explain how the name is generated, why import paths change it, and what the worker does with an unknown name, including that the message is discarded.
Show how to diagnose it with the worker banner and inspect registered, and how to rename or move tasks without losing queued work.
Treat task names as a public interface between deployables: explicit names for cross-service tasks and a rollout order where workers learn names before producers use them.
## How a task gets its name Every Celery task has a **name**, a string that must be unique within the application. The producer does not send the function; it sends a message whose header says which task to run by name, plus the arguments. So the name is the contract between the code that calls a task and the worker that runs it. If you do not pass `name=`, the decorator generates one from the **module the function is defined in** and the **function's name**, joined by a dot (`gen_task_name` in `celery/utils/imports.py`). Examples for a shop project: | Where the function is defined | Generated name | |---|---| | `shop/billing/tasks.py`, imported as `shop.billing.tasks` | `shop.billing.tasks.render_invoice_pdf` | | the same file, imported as `billing.tasks` | `billing.tasks.render_invoice_pdf` | | `@app.task(name='billing.render_invoice_pdf')` | `billing.render_invoice_pdf` | | a module run as a script (`__main__`) | the app's main name plus the function name | The second row is the surprise: the name depends on **how the module was imported**, not on the file path. Code that reaches the same file through two different `sys.path` entries produces two different names. The same rule applies to tasks declared with `@shared_task`: the name still comes from the defining module and function, and every app that registers the task uses that name. A class-based task registered with `app.register_task()` gets its module plus its class name, unless the class sets its own `name` attribute. Whatever the declaration style, the check is the same: the string in the message must match a key in the worker's `app.tasks` exactly, including every dot and underscore. ## What the worker does with the name At start-up a worker imports the modules it is told about (the `include` argument or the `imports` setting, or the framework's autodiscovery) and every `@app.task` it executes registers its name in `app.tasks`. When a message arrives, the worker looks the name up. If it is missing, the worker: 1. logs `Received unregistered task of type '<name>'`, with hints about imports; 2. **rejects the message without requeueing it**, so it is discarded (or dead-lettered, if the broker is set up for that); 3. records a `NotRegistered` failure for the task id in the result backend, when one is configured. The invoice is not retried later. From the shop's point of view, a customer silently never gets an invoice. ## Causes of an unregistered task - **The worker never imported the module.** The task lives in a module missing from `include` or `imports`, so the decorator never ran in the worker process. - **Two import paths.** The web process imports `shop.billing.tasks` while the worker was started from inside `shop/` and imports `billing.tasks`; both sides believe they have the same task, but the names differ. - **Renamed or moved code.** Messages already in the queue still carry the old name after `render_invoice` became `render_invoice_pdf`, or after the function moved to another module. - **Version skew.** A producer running newer code publishes a task the older workers do not have yet. - **Sending by a mistyped name.** `app.send_task('shop.billing.task.render_invoice_pdf', ...)` publishes whatever string you give it; no local check exists. ## Diagnosing it - Start the worker with `-l info` and read the `[tasks]` section of its banner, which lists every registered name. - Ask running workers with `celery -A shop inspect registered`. - In the producer, print `render_invoice_pdf.name` and compare it character for character. ## Making names stable - Give tasks that other code or queued messages depend on an explicit `name=`, so refactors that move the function do not change the contract. - Keep one canonical import path for task modules and start workers from the project root. - When moving a task, keep the old name registered until the queue has drained of old messages. - If a naming scheme is wanted project-wide, override `Celery.gen_task_name`; the docs require it to be a pure function, since producer and worker must compute the same name. `app.send_task(name, args)` is the one legitimate case for sending by string: the producer does not import the task code at all, typically because the worker lives in another codebase. There, the name is the whole interface, and it must be written down as carefully as an HTTP route.
- How do you safely rename a Celery task that has messages already queued under the old name?Keep the old name registered until the queue drains: leave a thin task under the old name that calls the new one, or give the moved function the old name via `name=`. Deploy workers that know both names before producers start sending the new one, then remove the alias once no old messages remain.
- Why does the same tasks.py file produce two different Celery task names in two processes?The generated name uses the module's import name, not its file path. If the web process imports `shop.billing.tasks` and the worker, started from inside the package directory, imports `billing.tasks`, the prefixes differ. A consistent working directory and `sys.path`, or an explicit `name=`, removes the mismatch.
saying these in an interview costs you the question
- The worker receives the task's code in the message, so it does not need to import it.
- An unregistered task is requeued until a worker that knows it picks it up.
- Task names come from the file path, so the same file always gets the same name.
- send_task checks locally that the task name exists before publishing.
- Renaming a task function is safe while old messages are still queued.