Multithreading in Odoo18: Running Background Tasks Efficiently
Introduction Odoo operates in a single-threaded environment, meaning that all processes execute sequentially. However, some operations, such as long-running computations, API calls, and large data…

Introduction

Odoo operates in a single-threaded environment, meaning that all processes execute sequentially. However, some operations, such as long-running computations, API calls, and large data imports, can block the system and slow down user interactions. Multithreading can be used to run such tasks in the background to improve responsiveness.

Python provides a threading module that allows developers to execute tasks asynchronously. However, since Odoo’s ORM is not thread-safe, developers must be careful when modifying the database from a separate thread. Below, we will explore how multithreading can be implemented in Odoo and the necessary precautions to avoid database conflicts.


Using Python’s threading.Thread in Odoo

The simplest way to execute a background task asynchronously is to use Python’s built-in threading.Thread. This allows tasks to run in the background while Odoo continues processing other user requests.

Example: Running a Function in a Separate Thread

python

import threading
class MyModel(models.Model):
    _name = ‘my.model’

    def start_background_task(self):
        """ Start a background thread safely """
        thread = threading.Thread(target=self._background_task, args=("param1", "param2"))
        thread.start()

    def _background_task(self, param1, param2):
        """ Simulating a long-running task """
        print(f"Processing {param1} and {param2}")

 Why use threading?

  • Prevents the main execution flow from being blocked.
  • Allows Odoo to continue handling user interactions while the task runs.

 Limitations:

  • Cannot directly interact with Odoo’s ORM safely from a thread.
  • No built-in error handling or retry mechanisms.
  • Cannot monitor or restart threads if they fail.

Thread Safety and ORM Transactions in Odoo

Odoo’s ORM is not thread-safe, which means concurrent database transactions from multiple threads can lead to:

  • Lost updates: One thread may override changes made by another.
  • Session crashes: Shared database cursors can cause integrity issues.
  • Deadlocks: Multiple threads accessing the same records simultaneously may lock the database.

Handling Database Operations in a Thread

To safely modify the database from a thread, a new environment must be created, and transactions must be committed manually.

 Example: Writing to the Database Safely in a Thread

python

import threading
from odoo import api, models
class MyModel(models.Model):
_name = ‘my.model’
def start_background_task(self):
""" Start a background thread safely """
thread = threading.Thread(target=self._background_task, args=(self.env.cr.dbname,))
thread.start()

@staticmethod
def _background_task(dbname):
""" Background task that interacts with the database safely """
with api.Environment.manage():
registry = odoo.registry(dbname)
with registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
partner = env[‘res.partner’].browse(1)
partner.write({‘name’: ‘Updated Name’})
cr.commit() # Ensure the transaction is committed

How does this work?

  • We pass the database name to the thread instead of the environment (self.env).
  • Inside the thread, we create a new Odoo environment to avoid conflicts.
  • A new database cursor (cr) is used to safely execute operations.
  • We manually commit transactions (cr.commit()) to ensure changes are saved.

Avoiding Common Mistakes

  • Never pass self.env to a thread directly. The same environment should not be used in multiple threads.
  • Always create a new environment inside the thread. This prevents session conflicts.

Limitations of Multithreading in Odoo

While multithreading can improve performance, it comes with several constraints:

  1. ORM is Not Thread-Safe
    • The ORM does not support concurrent database writes.
    • Using a shared environment across threads can lead to unpredictable behavior.
  2. Global Interpreter Lock (GIL)
    • Python’s GIL prevents true parallel execution of CPU-bound tasks in a single process.
    • Multithreading is better suited for I/O-bound tasks (e.g., API calls) rather than CPU-heavy computations.
  3. Thread Lifecycle Management
    • Threads cannot be monitored or restarted if they fail.
    • If a thread crashes, it does not get retried automatically.

When to Use Multithreading in Odoo

Good Use Cases:

  • Making API calls that do not interact with the database.
  • Sending emails or processing messages asynchronously.
  • Performing lightweight tasks like logging or data formatting.

Avoid for:

  • Database-intensive operations (use queue_job instead).
  • CPU-heavy tasks (use multiprocessing instead).
  • Any operation that requires monitoring or retries.

Conclusion

Multithreading in Odoo should be used cautiously due to ORM limitations. If your task involves database writes, ensure that:

  • A new Odoo environment is created inside the thread.
  • Transactions are committed manually to avoid inconsistencies.
  • The main thread is not blocked by long-running operations.

For simple tasks like making API calls, sending emails, or processing logs, multithreading can help improve performance. However, for database-intensive operations, other approaches like queue-based processing or multiprocessing are recommended.

Real Code Example(models/send_message.py):

import threading
import odoo
from odoo import api, models, fields, _
from jinja2 import Template
from odoo.exceptions import UserError, ValidationError

class SendMessageWizard(models.TransientModel):
_name = ‘send.message.wizard’
_description = ‘Send Message Wizard’

send_message_preview = fields.Text("Message Preview")
sms = fields.Boolean("Send SMS")

def send_message(self):
"""Start the SMS sending process in the background and close the wizard."""
if not self.sms:
raise UserError("Please select the SMS option to send messages.")

partner_ids = self.env.context.get(‘default_partner_ids’, [])
origin_from = self.env.context.get(‘origin_from’, ”)

if not partner_ids:
raise ValidationError("No contacts selected for sending messages.")

self.env[‘bus.bus’]._sendone(self.env.user.partner_id, ‘simple_notification’, {
‘type’: ‘success’,
‘title’: _("Success"),
‘message’: _(‘The SMS sending process is running in the background. Please check in message history for updates.’),
})

# Start the background process using threading
thread = threading.Thread(target=self._start_background_process, args=(partner_ids, origin_from))
thread.start()

# Close the wizard immediately
return {‘type’: ‘ir.actions.act_window_close’}

def _start_background_process(partner_ids, origin_from):
"""Start the SMS sending process in a new thread.
The method _start_background_process() is executed in a separate thread, ensuring that the UI remains responsive. The thread does not use self.env directly to avoid ORM conflicts."""
dbname = odoo.tools.config[‘db_name’] # Get database name

with api.Environment.manage():
registry = odoo.registry(dbname)
with registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
wizard = env[‘send.message.wizard’].create({}) # Create a temporary wizard
wizard._process_sms_in_background(partner_ids, origin_from)
cr.commit() # Ensure transaction is committed

def _process_sms_in_background(self, partner_ids, origin_from):
"""Processes SMS sending in a separate thread with a new database cursor.
Ensures safe execution by creating a new environment and committing transactions manually. """
with api.Environment.manage():
registry = odoo.registry(self.env.cr.dbname)
with registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})

# Fetch partner records using the IDs
partners = env[‘message.history’].browse(partner_ids) if origin_from == ‘message.history’ else env[‘res.partner’].browse(partner_ids)

if not partners:
raise ValidationError("No valid contacts with phone numbers were found.")

sms_data = {"messages": []}

for partner in partners:
if partner.phone:
if origin_from == ‘message.history’:
sms_data[‘messages’].append({
"PhoneNo": partner.phone,
"Message": partner.message,
"BatchNumber": partner.batch_number
})
else:
template = Template(self.send_message_preview or "Hello {{object.name}}, this is a test message.")
message = template.render(object=partner)
sms_data[‘messages’].append({
"PhoneNo": partner.phone,
"Message": message
})

# Send SMS via FastAPI
fastapi_integration = env[‘fastapi.integration’]
try:
url = env[‘tma.settings’].search([], limit=1).sms_service_url
if not url:
raise ValueError("SMS Service URL is not configured. Please set it in Settings.")
fastapi_integration.call_fastapi_endpoint(url, sms_data)
except Exception as e:
cr.rollback()
env[‘mail.message’].create({
‘body’: f"Error sending SMS batch: {str(e)}",
‘message_type’: ‘notification’,
‘model’: ‘send.message.wizard’,
})
else:
cr.commit() # Commit transaction