Your Odoo users don't complain about queues. They complain that the stock screen freezes while a report runs, invoices take too long to generate, shipping updates arrive late, or support tickets sit in the wrong state because one API call blocked everything behind it.
That's usually the core problem. Odoo is doing too much work inside the user request.
In growing SMEs, that pattern breaks fast. Finance wants month-end reports. Warehouse staff need immediate stock visibility. Sales expects customer updates to land without delay. If every heavy task runs inline, your ERP becomes the bottleneck instead of the control centre. A better queueing model fixes that.
Table of Contents
- Why Your Odoo ERP Needs a Better Task Queue
- Building a Simple Queue with Redis Lists
- Ensuring Reliability with Redis Streams
- Advanced Queueing Patterns in Redis
- Integrating Redis Queues into Your Odoo ERP
- Choosing Your Queue Redis vs RabbitMQ and Kafka
- Conclusion Building Resilient Odoo Systems
Why Your Odoo ERP Needs a Better Task Queue
The first sign is usually small. A user clicks “Validate”, “Post”, or “Generate PDF”, then waits. Other users feel it too because the expensive work is happening inside the same application flow that should stay responsive.
That's a bad fit for ERP. Odoo handles finance, stock, CRM, purchasing, manufacturing, and integrations in one system. If a report build, courier webhook, or batch synchronisation runs in the foreground, it competes with day-to-day operations that staff need right now.
A queue separates those concerns. The user triggers the action, Odoo records the intent, and a background worker processes the heavy part outside the web request. That one shift usually makes the system feel calmer and more predictable.
What should move out of the request
Some jobs belong in the background from day one:
- Document generation: large invoice PDFs, statements, export files, and compliance packs
- Third-party syncs: couriers, marketplaces, payment systems, EDI, and supplier APIs
- Bulk operations: stock recomputations, scheduled replenishment logic, mass email preparation
- Post-action automation: support SLA updates, follow-up emails, portal notifications, and audit logging
Practical rule: if a task can safely finish after the user gets a confirmation message, it probably belongs in a queue.
For Odoo teams, Redis is often the most practical starting point because it's simple to operate and fast enough for common ERP workloads. Redis can act as a high-performance queue with sub-millisecond latency and supports patterns such as PUSH plus LPOP or BLPOP for atomic retrieval, which is well suited to real-time task scheduling in distributed systems and Odoo-style integrations (Redis queue glossary).
If you're troubleshooting sluggish custom modules, work on root cause first. Basic request tracing and profiling and optimizing apps helps you separate ORM issues from queue-worthy background work. Then your automation strategy becomes much easier to design inside Odoo, especially if you're already thinking about ERP automation for Odoo SMEs.
Building a Simple Queue with Redis Lists
A common Odoo failure looks small at first. A sales order is confirmed, then the screen hangs while the system calls a courier API, updates stock in another system, and prepares a customer email. Users click twice, warehouse staff wait, and operations start drifting. A Redis List fixes that by moving the slow work out of the request and into a worker process.

How the list pattern works
Redis Lists are the fastest way to put a queue in place for Odoo. Odoo pushes a job onto one side of the list. A worker blocks until a job appears, then pulls it from the other side and processes it.
The pattern is simple:
- Odoo creates a small job payload
- Redis stores it in a list with
LPUSH - A worker waits with
BRPOP - The worker processes the job
That simplicity is the main benefit. For SME projects, it often solves the first queueing problem without adding much operational overhead. I use this pattern for tasks where the business wants faster screens first, and strict delivery guarantees can wait until the workflow proves its value.
In Odoo terms, this fits jobs such as:
- Webhook handling: accept the callback fast, then update stock or delivery status in the background
- Report generation: return control to the user, then build the PDF or export file outside the HTTP request
- Email and notifications: post the document now, send the email or call the external API a few seconds later
Python producer and consumer example
An Odoo-side producer can be very small:
import json
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def enqueue_odoo_job(model, record_id, action, extra=None):
payload = {
"model": model,
"record_id": record_id,
"action": action,
"extra": extra or {},
}
redis_client.lpush("odoo:jobs", json.dumps(payload))
Call this after the business transaction is committed. For example, after confirming a sales order, queue a courier booking or marketplace sync instead of making the user wait for an outside service.
A minimal worker looks like this:
import json
import redis
import time
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def process_job(job):
print(f"Processing {job['action']} for {job['model']}:{job['record_id']}")
time.sleep(1)
while True:
item = redis_client.brpop("odoo:jobs", timeout=0)
_, raw_payload = item
job = json.loads(raw_payload)
process_job(job)
BRPOP is the right default here. The worker sleeps until a job is available, so it does not poll Redis in a tight loop and waste CPU.
Keep the payload small. Model name, record ID, action name, and a short context dict are usually enough. That makes retries, inspection, and support work much easier when an SME client asks why a shipment update or invoice email did not run.
One more practical point. The worker should usually re-read the record from Odoo instead of trusting stale payload data. Queue the identifier, then let Odoo apply current business rules at execution time. That avoids odd cases where a customer address, stock reservation, or payment state changed between enqueue and processing.
Lists work well for first deployments and lighter background work. They improve response time quickly, and they are easy to explain to both developers and operations managers. For teams building custom Odoo development services, that makes them a good starting pattern.
The trade-off is reliability. Once a worker pops a job, Redis removes it from the list. If the worker crashes mid-task, the job is gone unless your application recorded enough state to recreate it. That limitation is acceptable for some internal utilities. It is a poor fit for invoices, stock movements, and customer-facing automations where missed work turns into real operational errors.
Ensuring Reliability with Redis Streams
Simple lists are attractive because they're quick to build. They're also the pattern often outgrown first.
In ERP, a lost task isn't just a technical issue. It can mean a shipment never syncs, a customer invoice never sends, or a support escalation never fires. Those failures are difficult because users often don't see them immediately. They surface later as stock mismatches, missed SLAs, and finance corrections.

Why lists stop being enough
The biggest weakness in list-based queues is crash recovery. A worker pops the job, starts work, then dies. The queue thinks the message is gone. Odoo still expects the outcome. Nobody notices until operations drift.
That's why Redis Streams are the better production choice for most serious Odoo queueing. Streams add consumer groups, pending message tracking, and acknowledgements. The worker only marks a message complete after the job has finished.
One practical analysis of Redis queue patterns points out that many guides oversimplify crash handling. When a worker fails mid-task, simple list queues can lose the task permanently, while Redis Streams can reclaim work with XAUTOCLAIM, with an estimated 40% reduction in data loss incidents in fault-tolerant systems according to the verified data provided.
A short walkthrough helps if you want to see the moving parts in action:
A safer worker pattern for Odoo jobs
With Streams, the flow changes:
- Odoo appends a message with
XADD - Workers read via a consumer group
- Redis tracks unacknowledged jobs in the pending list
- The worker calls
XACKafter success - Another worker can reclaim abandoned jobs if the first worker dies
Here's a simplified producer:
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def publish_stream_job(model, record_id, action):
redis_client.xadd(
"odoo:stream:jobs",
{
"model": model,
"record_id": str(record_id),
"action": action,
}
)
And a worker pattern:
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
STREAM = "odoo:stream:jobs"
GROUP = "odoo-workers"
CONSUMER = "worker-1"
try:
redis_client.xgroup_create(STREAM, GROUP, id="0", mkstream=True)
except redis.ResponseError:
pass
while True:
messages = redis_client.xreadgroup(
GROUP,
CONSUMER,
{STREAM: ">"},
count=1,
block=5000
)
for stream_name, entries in messages:
for message_id, fields in entries:
try:
model = fields[b"model"].decode()
record_id = int(fields[b"record_id"].decode())
action = fields[b"action"].decode()
print(f"Running {action} on {model}:{record_id}")
redis_client.xack(STREAM, GROUP, message_id)
except Exception:
pass
Don't acknowledge first and hope the rest works out. In Odoo, acknowledgement should come after the business action has succeeded and any required state update is committed.
Streams also fit multi-worker setups much better. Multiple workers can consume from the same group without duplicating normal work. That's useful when one Odoo database needs separate workers for stock syncs, document generation, and external API tasks.
This approach becomes even more important when you're responsible for Odoo hosting and background processing reliability. A queue that survives worker failure isn't a nice extra. It's part of operational control.
Advanced Queueing Patterns in Redis
FIFO isn't enough for many Odoo workflows. Some jobs are urgent. Some shouldn't run until later. Some need a safer handoff even if you're still using list-based structures.
Safer handoff with processing queues
If you're not ready to move fully to Streams, you can still improve on a basic list queue. A common pattern is to move the item from a pending queue to a processing queue, then remove it from the processing queue only after success.
That gives you a simple recovery path. If a worker dies, you can inspect the processing list and requeue stuck jobs. It's not as clean as Streams, but it's much better than popping and hoping.
This is useful for Odoo tasks such as:
- Import batches: process product updates in chunks and recover unfinished chunks
- Marketplace syncs: stop duplicate imports while still recovering interrupted jobs
- Warehouse scans: keep track of in-flight processing when barcode events arrive in bursts
Priority and delayed jobs with sorted sets
Sorted Sets solve a different problem. They let you decide not just what should run, but when and in what order.
Use priority scores when urgent business tasks must jump ahead. Use future timestamps when the job must wait until a specific moment.
Examples inside Odoo are straightforward:
- A stock-out alert should run before a nightly marketing sync
- A payment reminder should wait until the correct date
- A midnight reconciliation job should not compete with daytime user traffic
- A post-delivery follow-up should trigger after the delivery event, not immediately
A simple scheduling pattern looks like this:
import json
import time
import redis
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def schedule_job(run_at_ts, payload):
redis_client.zadd(
"odoo:scheduled:jobs",
{json.dumps(payload): run_at_ts}
)
def fetch_due_jobs(now_ts=None):
now_ts = now_ts or time.time()
due = redis_client.zrangebyscore("odoo:scheduled:jobs", 0, now_ts)
for raw in due:
redis_client.zrem("odoo:scheduled:jobs", raw)
yield json.loads(raw)
That pattern matters more than many queue guides suggest. The verified data notes that scheduled, delayed, and priority-queued tasks using Redis Sorted Sets are critical in UK retail and e-commerce ERP integrations, and that 42% of online order delays in logistics and wholesale sectors occur because background tasks lack priority differentiation or delay handling.
If every background job has the same urgency, your queue design is too simple for a real ERP.
In Odoo, priority-aware queueing improves outcomes in stock allocation, shipping label generation, procurement triggers, and customer communication. The technical win is cleaner scheduling. The business win is fewer operational collisions.
Integrating Redis Queues into Your Odoo ERP
The queue itself isn't the hard part. The hard part is choosing the right job boundary inside Odoo.

If you queue too little, the user still waits. If you queue too much, you create complicated state management and make debugging painful. The best jobs are discrete, traceable business actions with a clear success condition.
What to send into the queue
For most Odoo implementations, the job payload should stay small and explicit. Don't serialize half the record. Send enough information for the worker to fetch the latest state from Odoo and execute one action.
A practical payload often includes:
| Field | Purpose |
|---|---|
model |
Odoo model such as sale.order or stock.picking |
record_id |
The record to process |
action |
A named operation such as generate_pdf or push_tracking_update |
company_id |
Useful in multi-company databases |
context |
Small optional values, not full record dumps |
That keeps the worker logic deterministic. It also avoids processing stale business data that was captured too early.
A practical Odoo publisher and worker
Here's a conceptual publisher from a custom module:
import json
import redis
from odoo import models
class SaleOrder(models.Model):
_inherit = "sale.order"
def action_confirm(self):
result = super().action_confirm()
redis_client = redis.Redis(host="localhost", port=6379, db=0)
for order in self:
payload = {
"model": "sale.order",
"record_id": order.id,
"action": "send_to_shipping_connector",
"company_id": order.company_id.id,
}
redis_client.xadd("odoo:jobs", {"payload": json.dumps(payload)})
return result
A separate worker can then connect to Odoo through the ORM in a managed process or through XML-RPC if you prefer looser coupling:
import json
import redis
import xmlrpc.client
ODOO_URL = "http://localhost:8069"
DB = "mydb"
USERNAME = "worker@example.com"
PASSWORD = "secret"
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(DB, USERNAME, PASSWORD, {})
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
redis_client = redis.Redis(host="localhost", port=6379, db=0)
def run_job(payload):
if payload["model"] == "sale.order" and payload["action"] == "send_to_shipping_connector":
models.execute_kw(
DB, uid, PASSWORD,
"sale.order", "run_shipping_export",
[[payload["record_id"]]]
)
messages = redis_client.xread({"odoo:jobs": "0-0"}, count=1, block=0)
for stream, entries in messages:
for message_id, fields in entries:
payload = json.loads(fields[b"payload"].decode())
run_job(payload)
This model works well for several common ERP pain points:
- Large invoice output: generate and store PDFs outside the user request
- Courier webhooks: accept the callback quickly, queue the actual stock or tracking update
- Retail stock feeds: batch updates from POS terminals without hammering the main Odoo workers
- Support automation: process SLA events and notifications asynchronously
The verified data also gives a useful market signal here. Redis-based message queues have been integrated into 150+ custom Odoo ERP modules, reducing manual intervention by up to 40% and improving order fulfilment speed by 25% for mid-market companies in relevant UK digital transformation work (Redis Streams in custom Odoo modules).
For businesses connecting Odoo to couriers, portals, support tools, and commerce platforms, queue design is part of the wider Odoo integration strategy, not a side detail.
Choosing Your Queue Redis vs RabbitMQ and Kafka
Redis is not the only queueing option. It's just the one that fits a lot of Odoo workloads with the least friction.
Where Redis fits best
If your main goal is background jobs for one ERP platform, Redis is usually the shortest path to production. It's simple, fast, and easy for Python teams to operate. That matters when your priority is improving order processing, stock updates, and user responsiveness without introducing a heavyweight messaging platform.
Its strongest argument is latency. Redis shows sub-millisecond response times for latency-sensitive queue operations, while Kafka is stronger on throughput rather than low-latency job scheduling (benchmarking reference).
RabbitMQ and Kafka solve different problems well:
- RabbitMQ: better when you need richer broker-level routing and more formal messaging patterns
- Kafka: better when you need event streams at very high throughput and durable distributed logs
- Redis: better when Odoo needs a practical queue quickly and operations teams want less infrastructure overhead
There's another trade-off that often gets ignored. Network behaviour matters. Verified data notes that Redis dequeue rates can degrade by up to 10x over standard TCP for small payloads, and multi-worker concurrency plus batching helps recover throughput in real deployments. That's one reason to test workers under realistic load instead of trusting local-machine performance.
Queue technology comparison for Odoo workflows

| Criterion | Redis | RabbitMQ | Apache Kafka |
|---|---|---|---|
| Primary fit | Odoo background jobs, caching, light event workflows | Service-to-service messaging with more routing control | High-throughput event pipelines |
| Latency | Excellent for latency-sensitive jobs | Good | Good, but not the main strength here |
| Throughput | Strong for common ERP workloads | Strong | Best choice when throughput is the dominant requirement |
| Complexity | Low | Medium | High |
| Operational overhead | Usually lowest | Moderate | Highest |
| Best starting point for SMEs | Often yes | Sometimes | Rarely |
Choose the queue your team can operate properly. A theoretically better broker becomes worse the moment nobody can debug it at 6:30 on a warehouse shift.
For many Odoo environments, Redis is the practical first choice. If your wider stack already depends on message routing patterns or event streaming beyond ERP, then RabbitMQ or Kafka may fit better. If you're building cloud-connected business systems around Odoo, that decision should sit alongside your broader cloud solutions architecture.
Conclusion Building Resilient Odoo Systems
Redis as queue works best when you stop treating it as a clever add-on and start treating it as part of your ERP architecture. That means choosing the right pattern for the right job. Lists are fine for simple workloads. Streams are better where task recovery matters. Sorted Sets help when timing and priority affect real operations.
For Odoo, the benefit is direct. Users stop waiting on slow background work. Inventory updates land more cleanly. External integrations stop blocking sales and warehouse activity. Support workflows become easier to trust.
The biggest improvement isn't technical elegance. It's operational stability. Your ERP stops feeling fragile under load.
If you're responsible for uptime and worker behaviour around business-critical systems, this kind of background processing discipline sits well alongside broader reliability practice such as this guide for SRE and DevOps teams. The same thinking applies in Odoo. Measure failure paths, reclaim stuck work, and design for retries before production forces the issue.
If your Odoo system needs faster background processing, safer integrations, or a more reliable queue architecture, ERP Artists can help design and implement a Redis-backed approach that fits real SME operations in manufacturing, retail, logistics, finance, and support.