Fix: Celery Task Not Received or Not Executing
Part of: Python Errors
Quick Answer
Fix Celery tasks not being received or executed by resolving broker connections, autodiscovery issues, task name mismatches, and worker configuration.
The Error
You send a Celery task, but nothing happens. The task either never appears in the worker output, or you see:
Received unregistered task of type 'myapp.tasks.process_order'Or the task is sent successfully but the worker shows no activity:
result = process_order.delay(order_id=123)
print(result.id) # Returns a task ID
# But the worker never picks it upThe worker just sits idle, or the task gets silently dropped.
Why This Happens
Celery has multiple components that must all be configured correctly: the broker (Redis or RabbitMQ) that holds messages, the worker that processes them, and the application that sends them. A failure at any point in this chain causes tasks to be lost or ignored.
Common causes include the worker not connecting to the same broker as the sender, task names not matching between sender and worker, the autodiscovery mechanism not finding your task modules, or the worker listening on a different queue.
The trap with Celery is that the sender almost always succeeds. task.delay() writes a JSON message into the broker and returns an AsyncResult with an ID — that ID exists whether or not any worker will ever pick the message up. So the symptom (no execution) and the cause (no consumer matching the queue, or no registration matching the name) are separated by an entire system boundary. You cannot tell from the sender’s side whether the message was received, only whether it was queued. That is why this bug eats half a day if you debug from the wrong end. Always start the diagnosis at the worker, not the sender.
The other recurring trap is environment drift. The web process and the worker process are two separate Python interpreters with two separate sets of environment variables, two separate imports, and often two separate deploy targets. If CELERY_BROKER_URL is defined in the web’s .env but missing from the worker’s systemd unit, the worker silently falls back to amqp://localhost// and you sit there wondering why messages vanish. Treat the worker as a different machine from your web tier even when they share a host.
Diagnostic Timeline
Run these steps in order. They isolate the failure layer in under 15 minutes.
Minute 0 — Confirm the message was actually queued. Run redis-cli llen celery (or rabbitmqctl list_queues name messages). If the count is zero immediately after .delay(), the sender is misconfigured or pointing at a different broker. If the count is non-zero, the message exists and the worker is the problem.
Minute 2 — Ask the worker what it knows. Run celery -A myproject inspect registered. This prints every task name the worker has imported. If your task name is missing, the worker never autodiscovered the module. If the name is present but differs from what the sender uses (myapp.tasks.foo vs tasks.foo), you have a naming mismatch.
Minute 4 — Ask the worker which queues it consumes. Run celery -A myproject inspect active_queues. Compare against the queue your sender targets. If the sender writes to priority but the worker only consumes celery, the message sits forever.
Minute 7 — Verify both sides see the same broker. In a shell on the sender host, run python -c "from myproject.celery import app; print(app.conf.broker_url)". Repeat on the worker host. If the URLs differ — different host, different database number, different vhost — that is the bug.
Minute 10 — Ping the worker. Run celery -A myproject inspect ping. A response means the worker process is alive and reachable through the broker. No response means the worker is dead, started against a different broker, or behind a firewall.
Minute 12 — Check for the visibility-timeout silent redelivery. If tasks appear to run but never finish, the worker may be exceeding Redis’s default 1-hour visibility timeout. Redis redelivers the message, the worker picks it up again, and you see the same task ID appearing twice in flower. Set broker_transport_options = {'visibility_timeout': 43200} for long tasks.
Minute 15 — Rule out task_always_eager. Print app.conf.task_always_eager from the sender’s shell. If it is True, your tasks run synchronously in the web process and never touch the broker at all. This is the single most embarrassing root cause and the easiest to confirm.
Fix 1: Verify Broker Connection
Both the sender and worker must connect to the same broker. Check your Celery configuration:
# celery.py or config
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'Test the broker connection:
from celery import Celery
app = Celery('test', broker='redis://localhost:6379/0')
print(app.connection().ensure_connection(max_retries=3))If using Redis, verify it’s running:
redis-cli ping # Should return PONGCommon mistakes:
- Using
redis://localhostin the app but the worker is on a different machine - Using different Redis databases (
/0vs/1) - The broker URL is set in environment variables that aren’t loaded in the worker’s environment
Fix 2: Fix Task Autodiscovery
Celery’s autodiscover_tasks() finds tasks automatically, but it needs to know where to look:
# celery.py
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() # Searches INSTALLED_APPS for tasks.pyFor Django, this searches each app in INSTALLED_APPS for a tasks.py file. If your tasks are in a different file, they won’t be discovered:
# This is found automatically
myapp/tasks.py
# This is NOT found automatically
myapp/celery_tasks.py
myapp/workers/tasks.pyTo include non-standard task locations:
app.autodiscover_tasks(['myapp.workers', 'myapp.background'])Or register tasks explicitly:
# Include specific modules
app.conf.include = ['myapp.workers.tasks', 'myapp.background.jobs']Pro Tip: Run
celery -A myproject inspect registeredto see all tasks the worker knows about. If your task isn’t in the list, it hasn’t been discovered. Compare this list with what your sender expects.
Fix 3: Fix Task Name Mismatches
Celery generates task names based on the module path. If the sender and worker import tasks differently, the names won’t match:
# If the task is defined in myapp/tasks.py
@app.task
def process_order(order_id):
pass
# Celery names it: 'myapp.tasks.process_order'Problems arise when:
# Sender uses absolute import
from myapp.tasks import process_order
# Task name: 'myapp.tasks.process_order'
# But worker's autodiscovery finds it as
# Task name: 'tasks.process_order'Set an explicit name to avoid ambiguity:
@app.task(name='process_order')
def process_order(order_id):
passOr use send_task with the exact name:
app.send_task('myapp.tasks.process_order', args=[123])Check registered task names on the worker:
celery -A myproject inspect registeredFix 4: Verify the Worker Is Running
This sounds obvious, but it’s a common issue. The worker must be running and connected:
# Start the worker
celery -A myproject worker --loglevel=info
# Check if it's running
celery -A myproject inspect pingThe worker output should show:
[config]
.> app: myproject:0x...
.> transport: redis://localhost:6379/0
.> results: redis://localhost:6379/0
.> concurrency: 4 (prefork)
[queues]
.> celery exchange=celery(direct) key=celeryIf you see no output or connection errors, the worker can’t reach the broker. Check the broker URL and network connectivity.
For production, run the worker as a systemd service:
# /etc/systemd/system/celery.service
[Unit]
Description=Celery Worker
After=network.target
[Service]
Type=forking
User=celery
WorkingDirectory=/opt/myproject
ExecStart=/opt/myproject/venv/bin/celery -A myproject multi start worker1 \
--loglevel=info --logfile=/var/log/celery/%n%I.log
ExecStop=/opt/myproject/venv/bin/celery multi stopwait worker1Fix 5: Fix Queue Routing
If the task is routed to a specific queue but the worker listens on the default queue, the task is never picked up:
# Task routed to 'priority' queue
@app.task(queue='priority')
def urgent_task():
pass
# Or via routing configuration
app.conf.task_routes = {
'myapp.tasks.urgent_task': {'queue': 'priority'},
'myapp.tasks.*': {'queue': 'default'},
}The worker must explicitly consume from that queue:
# Only listens to 'celery' (default) queue
celery -A myproject worker
# Listen to specific queues
celery -A myproject worker -Q celery,priority
# Listen to all queues
celery -A myproject worker -Q celery,priority,defaultCheck which queues have pending messages:
# Redis
redis-cli llen celery # Default queue
redis-cli llen priority # Custom queue
# RabbitMQ
rabbitmqctl list_queues name messagesCommon Mistake: Sending a task to a queue that no worker consumes from. The messages pile up in the broker but are never processed. Always verify that at least one worker is listening on every queue you route tasks to.
Fix 6: Fix Serialization Issues
Celery serializes task arguments before sending them to the broker. If the arguments can’t be serialized, the task fails silently or raises an error:
# This fails with JSON serializer (default in Celery 4+)
@app.task
def process(data):
pass
process.delay(data=datetime.now()) # datetime isn't JSON serializableFix by converting arguments to serializable types:
process.delay(data=datetime.now().isoformat())Or change the serializer:
app.conf.task_serializer = 'pickle' # Handles more types
app.conf.accept_content = ['pickle', 'json']Warning: Using pickle is a security risk if your broker is accessible to untrusted clients. Stick with JSON and convert your data to serializable formats.
Check serialization compatibility:
import json
try:
json.dumps(your_task_arguments)
except TypeError as e:
print(f"Not JSON serializable: {e}")Fix 7: Disable task_always_eager for Production
task_always_eager runs tasks synchronously in the current process, bypassing the broker entirely. It’s useful for testing but causes confusion in production:
# settings.py
CELERY_TASK_ALWAYS_EAGER = True # Tasks run immediately, no worker neededIf this is enabled in production, tasks run in the web process and never reach the worker. Disable it:
# settings.py
CELERY_TASK_ALWAYS_EAGER = False # Default
CELERY_TASK_EAGER_PROPAGATES = FalseFor testing, use task_always_eager only in test settings:
# test_settings.py
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True # Propagate exceptions in testsFix 8: Tune Concurrency and Prefetch Settings
Workers may appear to not process tasks if they’re stuck on long-running tasks and the prefetch limit prevents new tasks from being fetched:
# Worker takes all available tasks but processes slowly
app.conf.worker_prefetch_multiplier = 4 # Default: fetches 4 × concurrency tasksFor long-running tasks, reduce prefetch:
app.conf.worker_prefetch_multiplier = 1 # Fetch one task at a time per worker processOr for tasks with unpredictable duration:
celery -A myproject worker --concurrency=4 -OfairThe -Ofair flag distributes tasks to workers that are actually free, rather than prefetching to all workers equally.
Check worker status:
celery -A myproject inspect active # Currently executing
celery -A myproject inspect reserved # Prefetched, waiting
celery -A myproject inspect stats # General statisticsStill Not Working?
Check the result backend. If you’re using
result.get()and the result backend isn’t configured, the call hangs forever. SetCELERY_RESULT_BACKENDor useresult.get(timeout=10).Look for import errors. If a task module has an import error, the worker skips it silently. Check worker startup output for import warnings.
Verify environment variables. If your tasks depend on env vars, ensure they’re available in the worker’s environment, not just the web process.
Check for task rate limits.
@app.task(rate_limit='10/m')limits execution to 10 per minute. Tasks beyond the limit are queued but delayed.Monitor with Flower. Install
flower(pip install flower) and runcelery -A myproject flowerfor a web dashboard showing task status, worker health, and queue depths.Check broker visibility timeout. For Redis, if a task takes longer than the visibility timeout (1 hour default), Redis redelivers it. Set
broker_transport_options = {'visibility_timeout': 43200}for long tasks.Restart workers after every deploy. Celery workers cache imported modules at startup. If you deploy new task code without restarting the worker, the worker keeps running the old code and may even reject new task names as “unregistered.” Wire
celery multi restartorsystemctl restart celeryinto your deploy script.Look for forked-child poisoning on macOS and Python 3.8+. The default prefork pool uses
fork(), which is unsafe with some C extensions (notably gRPC and some database drivers). The child dies silently on first use and you see no tasks executing. Switch to the thread pool with--pool=threadsor the gevent pool to confirm.Verify RabbitMQ vhost permissions. A user with read but not write permission on the vhost can publish but not ack, so messages reappear in the queue after every fetch. Run
rabbitmqctl list_user_permissions <user>and verify configure/write/read are all set to.*.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.
Fix: Celery Task Not Executing — Worker Not Processing Tasks
How to fix Celery tasks not executing — worker configuration, broker connection issues, task routing, serialization errors, and debugging stuck or lost tasks.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.