What is the Bus Service?
Odoo's bus service (implemented by the bus module) provides real-time server-to-client push notifications. When something changes on the server — a new message arrives, a record is updated, or a background task completes — the bus pushes a notification to connected browser clients instantly, without requiring page refreshes or polling.
This powers Odoo's live chat, real-time discuss messaging, notification badges, and presence indicators.
How It Works
Architecture
| Component | Role |
|---|---|
| bus.bus model | Server-side notification queue (PostgreSQL table) |
| Longpolling/WebSocket | Persistent connection from browser to server |
| bus_service (JS) | Client-side service that receives and dispatches notifications |
| Channels | Logical groupings for notification routing |
Flow
- Server code calls
self.env['bus.bus']._sendone(channel, type, message) - A record is created in the
bus_bustable - PostgreSQL NOTIFY triggers the longpolling/WebSocket controller
- The controller pushes the notification to all clients subscribed to that channel
- The client-side bus_service dispatches the notification to registered handlers
Server Side: Sending Notifications
Send to a Specific User
# Send to a specific user's partner
self.env['bus.bus']._sendone(
self.env.user.partner_id, # channel (partner record)
'my_module.update', # notification type
{ # payload (JSON-serializable dict)
'message': 'Your report is ready.',
'record_id': record.id,
'action': 'download',
},
)Send to All Users
# Broadcast to all connected users
self.env['bus.bus']._sendone(
'broadcast', # special channel name
'my_module.announcement',
{'message': 'System maintenance in 30 minutes.'},
)Send to Multiple Channels
# Send to multiple users at once
channels = [(partner, 'my_module.update', payload)
for partner in partner_ids]
self.env['bus.bus']._sendmany(channels)Client Side: Receiving Notifications
In an OWL Component
/** @odoo-module **/
import { Component, onWillStart, onWillUnmount } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
class LiveDashboard extends Component {
static template = "my_module.LiveDashboard";
setup() {
this.busService = useService("bus_service");
this.orm = useService("orm");
// Subscribe to notification channel
this.busService.addChannel("my_module_live");
// Listen for specific notification types
this.busService.addEventListener(
"notification",
this.onNotification.bind(this),
);
onWillUnmount(() => {
this.busService.removeEventListener(
"notification",
this.onNotification.bind(this),
);
});
}
onNotification({ detail: notifications }) {
for (const { payload, type } of notifications) {
if (type === "my_module.update") {
this.handleUpdate(payload);
}
}
}
handleUpdate(payload) {
// Update component state with new data
console.log("Received update:", payload);
// Refresh dashboard data
this.loadData();
}
}Common Use Cases
Live Record Updates
Notify clients when a record they are viewing has been modified by another user:
# In the model's write method
def write(self, vals):
result = super().write(vals)
for record in self:
self.env['bus.bus']._sendone(
'my_module_record_%s' % record.id,
'my_module.record_updated',
{'record_id': record.id, 'fields': list(vals.keys())},
)
return resultBackground Task Completion
Notify the user when a long-running task finishes:
# In the cron or background task
def _process_report(self):
# ... long processing ...
report_url = self._generate_report()
# Notify the user who requested the report
self.env['bus.bus']._sendone(
self.requested_by.partner_id,
'my_module.report_ready',
{
'title': 'Report Ready',
'message': 'Your sales report has been generated.',
'url': report_url,
},
)Progress Indicators
# Send progress updates during long operations
for i, record in enumerate(records):
record.process()
if i % 10 == 0: # Update every 10 records
self.env['bus.bus']._sendone(
self.env.user.partner_id,
'my_module.progress',
{'current': i + 1, 'total': len(records)},
)
self.env.cr.commit() # Commit so the bus notification is sentLongpolling vs WebSocket
| Feature | Longpolling | WebSocket |
|---|---|---|
| Connection | HTTP request held open | Persistent bidirectional |
| Efficiency | Less efficient (reconnects) | More efficient (single connection) |
| Configuration | Works out of the box | Requires gevent worker mode |
| Proxy support | Widely supported | Needs proxy WebSocket support |
Odoo 17+ prefers WebSocket when available. Configure with --gevent-port for the WebSocket/longpolling worker.
Infrastructure Requirements
- Separate gevent worker process for longpolling (--gevent-port 8072)
- Nginx proxy must forward /websocket to the gevent worker
- PostgreSQL LISTEN/NOTIFY for inter-process communication
- If using multiple Odoo workers, all must connect to the same PostgreSQL instance
Common Pitfalls
- Missing gevent worker — Without a dedicated longpolling worker, bus notifications do not work. Configure --gevent-port in your Odoo startup.
- Nginx WebSocket proxy — Nginx must include
proxy_set_header Upgrade $http_upgradeandproxy_set_header Connection "Upgrade"for the /websocket path. - cr.commit() for progress — Bus notifications are only sent after the transaction commits. For progress updates, commit periodically.
- Payload size — Keep notification payloads small. Send IDs and let the client fetch full data, rather than sending entire records.