Why Cache in Odoo?
Some computations are expensive and produce the same result for the same inputs — configuration lookups, permission checks, formatting rules. Odoo provides the @ormcache decorator family to cache method results at the registry level (shared across all requests in the same worker).
The @ormcache Decorator
The basic @ormcache caches method results based on the method arguments:
from odoo.tools import ormcache
from odoo import models, api
class IrConfigParameter(models.Model):
_inherit = 'ir.config_parameter'
@api.model
@ormcache('key')
def get_param(self, key, default=False):
params = self.search_read(
[('key', '=', key)],
fields=['value'],
limit=1
)
return params[0]['value'] if params else defaultKey points:
- The decorator argument
'key'is the parameter name used as cache key - The cache persists across requests within the same worker process
- The cache is per-database (Odoo includes the database name automatically)
- Results are stored in a dictionary keyed by the argument values
@ormcache_context
When the result depends on context values (like language or company), use @ormcache_context:
from odoo.tools import ormcache_context
class IrTranslation(models.Model):
_inherit = 'ir.translation'
@api.model
@ormcache_context('model_name', keys=('lang',))
def get_field_string(self, model_name):
# Result varies by language
return self._get_field_translations(model_name)The keys parameter specifies which context keys are included in the cache key. Different languages get different cache entries.
@ormcache_multi
For methods that accept multiple IDs and return results per ID:
from odoo.tools import ormcache_multi
class ResCompany(models.Model):
_inherit = 'res.company'
@ormcache_multi('self', 'ids')
def _get_company_settings(self, ids):
result = {}
for company in self.browse(ids):
result[company.id] = {
'currency': company.currency_id.name,
'country': company.country_id.code,
}
return resultThis caches results per individual ID. If you request IDs [1, 2, 3] and ID 1 is cached, it only computes for [2, 3].
Cache Invalidation
The most critical aspect of caching. Odoo provides explicit cache clearing:
class IrConfigParameter(models.Model):
_inherit = 'ir.config_parameter'
def write(self, vals):
res = super().write(vals)
# Clear the cache after writing
self.env.registry.clear_cache()
return res
def create(self, vals):
res = super().create(vals)
self.env.registry.clear_cache()
return res
def unlink(self):
res = super().unlink()
self.env.registry.clear_cache()
return resself.env.registry.clear_cache() clears ALL ormcache entries for the current database. This is a nuclear option — use it when you cannot selectively invalidate.
Selective Invalidation
For more targeted invalidation, you can clear specific caches:
# Clear cache for a specific method
type(self).get_param.clear_cache(self)
# Or clear the entire registry cache
self.env.registry.clear_cache()When to Use @ormcache
Good candidates for caching:
- Configuration parameters:
ir.config_parametervalues rarely change - Permission lookups: Group membership checks
- Format strings: Date/number formatting per locale
- Static reference data: Country codes, currency symbols
- Computed constants: Values derived from settings that change infrequently
Poor candidates:
- Transactional data: Order totals, stock levels — change frequently
- User-specific data: Unless you include the user ID in the cache key
- Large return values: Caching large objects wastes memory
- Methods with side effects: Caching skips the method body on cache hit
Cache Scope and Lifetime
| Cache Type | Scope | Lifetime | Cleared By |
|---|---|---|---|
| @ormcache | Registry (per worker) | Until clear or restart | clear_cache(), worker restart |
| ORM prefetch | Environment (per request) | Single request | invalidate_all(), end of request |
| Python lru_cache | Process | Until process restart | cache_clear() |
Important: @ormcache is per-worker. In a multi-worker setup, clearing cache on one worker does not clear it on others. Changes to ir.config_parameter signal all workers to clear via the bus, but custom caches may not have this mechanism.
Multi-Worker Cache Consistency
Odoo uses a registry signaling mechanism to propagate cache clears across workers. When you call registry.clear_cache(), it sets a signal that other workers detect on their next request. However, there can be a brief window where different workers have different cache states.
For critical consistency, consider using ir.config_parameter (which has built-in cross-worker signaling) instead of building your own caching layer.
Debugging Cache Issues
# Check cache statistics
cache = type(self).get_param.cache
print(f'Cache hits: {cache.hits}, misses: {cache.misses}')
print(f'Cache size: {len(cache.data)}')
# Force cache miss for testing
type(self).get_param.clear_cache(self)
result = self.get_param('my.key') # guaranteed freshCommon Pitfalls
- Forgetting invalidation: Cached values become stale if you do not clear the cache when underlying data changes
- Caching mutable objects: If you cache a list or dict, callers can modify the cached object. Return copies or use frozen types
- Cache keys with recordsets: Never use recordsets as cache keys — they are mutable and unhashable. Use
self.idortuple(self.ids) - Memory leaks: Unbounded caches grow forever. Odoo ormcache has size limits, but custom caches may not
- Testing: Tests may see stale cache from previous test methods. Clear caches in setUp