Skip to content

Fix Odoo Computed Field Recursion Loop: Infinite Recomputation and Dependency Cycles

DeployMonkey Team · March 23, 2026 10 min read

The Recomputation Loop Problem

Unlike a simple RecursionError that crashes immediately, a recomputation loop is subtler and more dangerous. The computed field keeps recalculating without hitting Python's recursion limit — instead, it causes extremely slow saves, high CPU usage, or hangs that eventually time out. This guide focuses on these silent infinite loops rather than the hard crash scenario.

Symptoms of a Recomputation Loop

  • Saving a record takes 30+ seconds or times out
  • CPU spikes to 100% when editing specific fields
  • Database shows hundreds of UPDATE queries for the same table
  • Odoo worker gets killed for exceeding time limits
  • No error in logs — just slow or stuck behavior

What the Slow Query Looks Like

# In PostgreSQL logs with log_min_duration_statement = 1000:
LOG: duration: 45123.456 ms  statement: UPDATE sale_order SET
    total_margin = ..., margin_percent = ..., write_date = ...
    WHERE id IN (1, 2, 3, ... 500)

# In Odoo logs:
WARNING odoo.models: Recomputation of field 'sale.order.total_margin'
    triggered recomputation of 'sale.order.margin_percent'
    which triggered recomputation of 'sale.order.total_margin'
    (cycle detected, stopping after 100 iterations)

Cause 1: Write Inside Compute Method

The most dangerous pattern. A compute method that writes to fields on the same record triggers recomputation of dependent fields, which may trigger the original compute again.

# BAD: write() inside compute triggers recomputation loop
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    risk_level = fields.Selection(
        [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')],
        compute='_compute_risk', store=True
    )

    @api.depends('amount_total')
    def _compute_risk(self):
        for rec in self:
            level = 'high' if rec.amount_total > 10000 else 'low'
            rec.risk_level = level
            # BAD — this write triggers field recomputation!
            rec.write({'note': f'Risk updated to {level}'})

# FIX: Never call write() inside a compute method
# Set all needed fields directly:
    @api.depends('amount_total')
    def _compute_risk(self):
        for rec in self:
            rec.risk_level = 'high' if rec.amount_total > 10000 else 'low'

Cause 2: Inverse Method Triggering Compute

Inverse methods allow writing to computed fields. But if the inverse method modifies a field that the compute method depends on, you get a loop.

# BAD: inverse writes to a field that compute depends on
class Product(models.Model):
    _inherit = 'product.template'

    margin_amount = fields.Float()
    sale_price = fields.Float(
        compute='_compute_sale_price',
        inverse='_inverse_sale_price',
        store=True
    )

    @api.depends('standard_price', 'margin_amount')
    def _compute_sale_price(self):
        for rec in self:
            rec.sale_price = rec.standard_price + rec.margin_amount

    def _inverse_sale_price(self):
        for rec in self:
            # BAD: writing margin_amount triggers _compute_sale_price again
            rec.margin_amount = rec.sale_price - rec.standard_price

# FIX: Use a context flag to break the cycle
    def _inverse_sale_price(self):
        for rec in self:
            rec.with_context(skip_price_compute=True).margin_amount = (
                rec.sale_price - rec.standard_price
            )

    @api.depends('standard_price', 'margin_amount')
    def _compute_sale_price(self):
        if self.env.context.get('skip_price_compute'):
            return
        for rec in self:
            rec.sale_price = rec.standard_price + rec.margin_amount

Cause 3: Onchange Triggering Related Compute

An onchange method modifies a field that a stored computed field depends on, causing recomputation during form editing.

# BAD: onchange modifies a dependency of a stored computed field
@api.onchange('partner_id')
def _onchange_partner(self):
    # This triggers recomputation of all fields that depend on pricelist_id
    self.pricelist_id = self.partner_id.property_product_pricelist
    # Which triggers price recomputation for all order lines
    # Which triggers total recomputation
    # Which may trigger other computations...

# FIX: Be aware of the chain reaction. If performance is an issue:
# 1. Use @api.depends instead of @api.onchange where possible
# 2. Batch the changes instead of setting fields one by one
# 3. Minimize stored computed fields that depend on frequently-changed fields

Cause 4: Cross-Model Dependency Chains

Field A on Model X depends on field B on Model Y, which depends on field C on Model X. The cycle spans multiple models, making it hard to spot.

# Model: sale.order
total_weight = fields.Float(compute='_compute_weight', store=True)

@api.depends('order_line.product_weight')
def _compute_weight(self):
    ...

# Model: sale.order.line
product_weight = fields.Float(compute='_compute_product_weight', store=True)

@api.depends('order_id.warehouse_id')  # depends back on the order!
def _compute_product_weight(self):
    # Different warehouses have different packaging weights
    ...

# FIX: Break the cross-model cycle
# Make one of the fields non-stored, or
# remove the back-reference dependency

How to Detect Loops

# 1. Enable SQL logging to see repeated queries:
./odoo-bin --log-handler=odoo.sql_db:DEBUG 2>&1 | grep UPDATE | head -50

# 2. Use Python profiling to find the hot function:
import cProfile
cProfile.run('record.write({"field": value})')

# 3. Check field dependencies in shell:
env['sale.order']._field_computed  # Shows which compute method handles each field
for name, field in env['sale.order']._fields.items():
    if hasattr(field, 'depends') and field.depends:
        print(f'{name}: {field.depends}')

# 4. Look for write() calls in compute methods:
grep -rn 'def _compute' my_module/ -A 10 | grep '\.write('

Prevention Rules

  • Never call write(), create(), or unlink() inside a compute method
  • Keep dependency chains short — ideally one level deep
  • Draw the dependency graph before adding stored computed fields
  • Use non-stored computed fields when you do not need search/sort
  • Test save performance with realistic data volumes, not just one record
  • Monitor SQL queries during saves to catch loops early