Why Performance Matters For Business Outcomes
Speed is not a vanity metric. In Odoo 19, faster screens and snappier operations reduce abandoned carts, shorten customer service handling time, and raise first-contact resolution. Tie each optimization to a KPI before you touch a line of code. Start with Odoo 19 logging/profiling (cProfile, query count) to see where time is actually lost. Optimization that is not traced is guesswork.
If you’re focusing on performance from the development side, it’s equally important to see how Odoo 19 enhances customer operations. Check out our in-depth article on Odoo 19 CRM Module: Better Customer Management to explore its real-world impact on client workflows.
Map slow features to KPIs and SLAs
Pick business flows that move the needle: checkout lead time, invoice validation per hour, or CRM pipeline transitions. Map the slowest forms and endpoints to measurable SLAs, then align technical changes like Reduce SQL queries with read_group / prefetch toward those goals.
Choose targets before tuning with Odoo 19 logging/profiling (cProfile, query count)
Enable query count and cProfile in dev. Keep a baseline JSON of timings. Only accept a change if the post-change timings beat the baseline. That is how you build confidence in your Odoo 19 ORM optimization decisions.
Baseline First - Measure Before You Tune
Without a baseline you will move regressions to production. Instrument the stack end to end with Odoo 19 logging/profiling (cProfile, query count) and capture RPC latencies.
Enable query count and SQL logs in dev
Turn on SQL logs for a short window. Use query count to catch N+1 patterns where forms trigger dozens of queries. This is the quickest win for Odoo 19 caching and prefetch strategies later.
Profile Python hot paths with cProfile and flame graphs
Wrap suspect endpoints with cProfile. Export stats to flame graphs so you can see slow functions that are invisible at the SQL layer. This is where Batch create/write to reduce RPC overhead often shows up as a top fix.
Capture APM style timings on endpoints and RPC calls
Record controller timings per route. Track p95 and p99, not just averages. It keeps you honest when traffic spikes.
Database Foundations - Odoo 19 PostgreSQL performance tuning
Postgres is the engine for your ERP. Tune it like one. Begin with Odoo 19 PostgreSQL performance tuning and keep autovacuum healthy.
Essential postgres.conf settings for throughput and latency
Right-size shared buffers, work mem, and effective cache size. Use connection pooling to avoid fork overhead on spikes. Verify plans with EXPLAIN ANALYZE.
Connection pooling and autovacuum hygiene
PgBouncer in transaction mode keeps connection counts under control. Autovacuum must run. If bloat grows, your read paths will slow and your write-amplification will hurt crons that should be quick.
Index strategy for domains, many2one, and date fields
Add indexes on domains used by record rules and common searches. Many2one and date ranges are frequent bottlenecks.
Code Method - migration-safe SQL index creation in a module
# migrations/16.0.1.0/post-migration.py or any versioned migrate script
def migrate(cr, version):
cr.execute"""
CREATE INDEX IF NOT EXISTS idx_sale_order_date
ON sale_order(date_order);
""")
cr.execute("""
CREATE INDEX IF NOT EXISTS idx_move_line_partner
ON account_move_line(partner_id);
""")
ORM Patterns That Scale - Odoo 19 ORM optimization
Write for sets, not loops. Odoo 19 ORM optimization is about vectorization and fewer round trips.
Prefer vectorized operations over per-record loops
Replace per-record writes with batched writes. Avoid browsing singletons in tight loops.
Reduce SQL queries with read_group and prefetch
Summaries belong in read_group. Prefetch fields used repeatedly to cut query count.
Avoid n+1 with mapped, with_context, and fields prefetch
Use records.mapped('field') and prefetch related fields. It pairs nicely with Odoo 19 caching and prefetch strategies.
Code Method - bulk read and grouped aggregation
# Example: sales totals by salesperson this monthdomain = [('date_order', '>=', fields.Date.to_string(fields.Date.today().replace(day=1)))]
data = self.env['sale.order'].read_group(
domain=domain,
fields=['user_id', 'amount_total:sum'],
groupby=['user_id'],
)
totals = {d['user_id'][0]: d['amount_total'] for d in data}
Write Efficiency - Batch create/write to reduce RPC overhead
Network hops are expensive. Favor Batch create/write to reduce RPC overhead to cut latency.
Chunked writes and safe batching patterns
Batch in chunks of a few thousand rows depending on model constraints. Validate chunk success and retry failed chunks.
Minimizing onchange overhead in forms
Use lightweight @api.onchange or defer expensive validations to create or write. Keep onchange pure and fast.
Code Method - batch create with vals_list
vals_list = [{'name': f'Tag {i}', 'color': i % 10} for i in range(5000)]tags = self.env['crm.tag'].create(vals_list) # single RPC, single commit
Smarter Computations - Computed fields performance (store=True, dependencies)
Use Computed fields performance (store=True, dependencies) wisely. Store only if read far more than written.
When to store computed fields and how to define deps
store=True with precise @api.depends cuts recomputes dramatically for dashboards.
Avoiding recompute storms
Keep dependency graphs narrow. Use guards to skip unnecessary work.
Code Method - stored compute with @api.depends best practice
from odoo import models, fields, api
class SaleOrder(models.Model):
_inherit = 'sale.order'
margin_rate = fields.Float(
compute='_compute_margin_rate',
store=True
)
@api.depends('amount_untaxed', 'order_line.margin')
def _compute_margin_rate(self):
for rec in self:
revenue = rec.amount_untaxed or 0.0
margin = sum(rec.order_line.mapped('margin'))
rec.margin_rate = margin / revenue if revenue else 0.0
Security Without Slowness - Speed up record rules and access rights
Complex domains can strangle performance. Use Speed up record rules and access rights with denormalized flags.
Designing fast domains and avoiding cross-model bottlenecks
Anchor rules on indexed fields. Avoid domains that traverse multiple relations for every query.
Using computed booleans and denormalized flags
Precompute a boolean like is_visible_to_user and store it. Then the rule is a single indexed check.
Code Method - cached helper for rule evaluation
from functools import lru_cache
@lru_cache(maxsize=1024)
def _can_see_category(user_id, category_id):
# pure check, memoized during a request
return category_id in user_allowed_category_ids(user_id)
Caching That Helps - Odoo 19 caching and prefetch strategies
Cache only what is cheap to invalidate. Odoo 19 caching and prefetch strategies shine when cache keys respect context and company.
Environment cache, LRU behaviors, and invalidation
Rely on Odoo’s env cache for records and fields. Do not reinvent it unless you have a pure function.
Safe memoization of pure functions
Memoize small reference data keyed by company and user.
Code Method - functools.lru_cache with context-aware keys
from functools import lru_cache
def _key(env):
return (env.user.id, env.company.id)
@lru_cache(maxsize=512)
def fiscal_defaults(user_company_key):
user_id, company_id = user_company_key
# fetch small reference set once
return {'tax_id': 15, 'journal_id': 8}
defaults = fiscal_defaults(_key(self.env))
Frontend Rendering - Optimize QWeb template rendering (t-call and t-foreach)
Templates can be hot. Use Optimize QWeb template rendering (t-call / t-foreach) to precompute data and reduce loops.
Reduce loop work and avoid heavy RPCs inside templates
Collect all data in the controller or model method, then render once.
Use t-set and fragments for reuse
Cache fragments with t-set to avoid recomputing inside nested loops.
Code Method - efficient t-foreach with precomputed data
<t t-name="my.list">
<t t-set="rows" t-value="precomputed_rows"/>
<ul>
<t t-foreach="rows" t-as="r">
<li><t t-esc="r['label']"/></li>
</t>
</ul>
</t>
Web Performance - Odoo 19 asset bundling and lazy loading (Website)
Static assets matter. Apply Odoo 19 asset bundling & lazy loading (Website) to shrink payloads.
Bundle CSS and JS, defer non-critical scripts
Use the asset pipeline to combine and minify. Defer analytics and heavy widgets.
Image formats, lazy loading, and CDN hints
Prefer WebP and add width and height. Enable lazy loading on below-the-fold images.
Code Method - manifest assets and lazy load example
># __manifest__.py
'assets': {
'web.assets_frontend': [
'my_mod/static/src/scss/site.scss',
'my_mod/static/src/js/site.js',
],
}
<img src="/web/image/12345" loading="lazy" width="640" height="360" alt="sample"/>
Concurrency Model - Odoo 19 workers and gevent configuration
Match workers to CPU cores. Odoo 19 workers & gevent configuration helps you scale reads and writes safely.
Sizing workers for CPU bound and IO bound workloads
Start with workers equal to 2 x cores and tune. Keep watchdogs and limits sensible to avoid thrashing.
Longpolling worker and timeout guidance
Run a dedicated longpolling worker on a separate port. Keep timeouts balanced for chat and notifications.
Code Method - example odoo.conf worker settings
[options]
workers = 6
max_cron_threads = 2
longpolling_port = 8072
limit_request = 8192
limit_memory_soft = 2147483648
limit_memory_hard = 2684354560
proxy_mode = True
Background Load - Cron job scheduling and queue performance in Odoo 19
Crons should not collide. Use Cron job scheduling & queue performance in Odoo 19 to stagger load.
Staggering heavy jobs and using job queues
Schedule large jobs at off-peak minutes. Use chunking to avoid locks on big tables.
Idempotency keys and retry policies
Design jobs to resume from last checkpoint and reprocess safely.
Code Method - safe cron with chunked processing
def _process_batch(self, offset=0, limit=500):
domain = [('state', '=', 'pending')]
batch = self.env['my.model'].search(domain, offset=offset, limit=limit)
for rec in batch:
rec.process_safely()
if len(batch) == limit:
self.env.cr.commit()
self._process_batch(offset=offset + limit, limit=limit)
Real Time Features - Longpolling, bus, and Live Chat performance tuning
For chat and notifications, tune Longpolling/bus & Live Chat performance tuning to keep response times low.
When to offload to websockets or message queues
If message volume is high, consider a queue for heavy work and leave Odoo to signal completion.
Timeout, heartbeat, and reconnection strategies
Short heartbeats lower ghost sessions. Test reconnections under packet loss.
Code Method - lightweight bus notifications
channel = (self._cr.dbname, 'res.partner', self.env.user.id)
payload = {'type': 'refresh_view', 'model': 'sale.order'}
self.env['bus.bus']._sendone(channel, payload)
Analytics At Scale - Dashboard and Spreadsheet performance (filters, pivots)
Heavy analytics can drown the database. Apply Dashboard & Spreadsheet performance (filters, pivots) with pre-aggregations.
Pre-aggregations and views for heavy reports
Create materialized views that store totals per day, partner, or product.
Limit data scopes and paginate dashboards
Default to the last 30 days and offer quick filters for more.
Code Method - materialized view refresh job
def setup_mv(cr):
cr.execute("""
CREATE MATERIALIZED VIEW IF NOT EXISTS sales_mv AS
SELECT user_id, date_trunc('day', date_order) AS day, sum(amount_total) AS total
FROM sale_order
GROUP BY user_id, day;
""")
def refresh_mv(cr):
cr.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY sales_mv;")
Edge And Network - Nginx reverse proxy and gzip settings for Odoo 19
Your proxy is a performance lever. Use Nginx/reverse proxy & gzip settings for Odoo 19 to shave milliseconds.
Keepalive, buffers, compression, and caching headers
Enable gzip for text assets. Add far-future cache headers for hashed static files.
Handling file uploads and timeouts cleanly
Set sane client_max_body_size and read timeouts to protect workers.
Code Method - sample nginx site config
server {
listen 80;
server_name example.com;
proxy_read_timeout 720s;
proxy_connect_timeout 60s;
proxy_send_timeout 720s;
location /longpolling/ {
proxy_pass http://127.0.0.1:8072;
}
location / {
proxy_pass http://127.0.0.1:8069;
gzip on;
gzip_types text/css application/javascript application/json;
}
location ~* /web/assets/ {
expires 30d;
add_header Cache-Control "public";
}
}
Deployment Playbook - Repeatable, testable performance
Production stability comes from rehearsal. Bake Odoo 19 workers & gevent configuration and DB settings into code, not tribal knowledge.
Staging load tests with sanitized data
Rehearse with realistic data sizes. Measure p95 before changes go live.
Canary releases and rollout metrics
Release to a small group, confirm metrics, then scale. Keep a rollback plan ready.
Code Method - pytest-benchmark and locust snippet
# tests/test_perf.pydef test_partner_search(benchmark, env):
def _run():
env['res.partner'].search([('name', 'ilike', 'a')], limit=200)
result = benchmark(_run)
# locustfile.pyfrom locust import HttpUser, task
class OdooUser(HttpUser):
@task
def open_pipeline(self):
self.client.get("/web#action=crm.crm_lead_action_pipeline")
Mini Case Study - From 3.2s to sub-second page loads
Map the improvement to business metrics like conversion and agent throughput.
Results and lessons mapped to KPIs
Apply the smallest code change that moves the metric. Keep a changelog with timings.
Actions taken across DB, ORM, and frontend
Hypothesize across layers: DB index gaps, ORM loops, frontend payloads.
Problem summary and hypotheses
A B2B portal showed 3.2 second median load on quotations. We profiled with Odoo 19 logging/profiling (cProfile, query count) and found N+1 queries in a QWeb loop, missing indexes on partner fields, and synchronous assets loading. Fixes included Reduce SQL queries with read_group / prefetch, adding two indexes, and Odoo 19 asset bundling & lazy loading (Website). Median dropped to 0.9 seconds, and abandoned quote edits fell by 18 percent.
Governance - Guardrails to keep performance healthy
Make speed part of your definition of done. Add checks for Speed up record rules and access rights and Computed fields performance (store=True, dependencies) in code reviews.
Code review checklist for performance smells
Look for loops calling search repeatedly, unbounded domains, and heavy computes.
Observability dashboards and alert thresholds
Watch p95 latency, cron runtimes, and queue depth. Alert before users complain.
Conclusion - A pragmatic path to a faster Odoo 19
Start with measurement, fix the biggest bottleneck, and retest. Repeat. Use Odoo 19 ORM optimization for server logic and Nginx/reverse proxy & gzip settings for Odoo 19 for the edge. The compound effect of small, verified wins will keep your ERP responsive under real-world load.
If your team wants a focused assessment aligned to your KPIs, book a 30-minute Odoo 19 performance review. We will profile, prioritize, and deliver a short action plan you can implement this week.
Frequently Asked Questions
Top dev questions on tuning and safety
Always baseline with Odoo 19 logging/profiling (cProfile, query count). Back out changes that do not move p95 in the right direction. Favor small PRs and feature flags.
When to choose compute store and when not to
Apply Computed fields performance (store=True, dependencies) when reads far outnumber writes. If the value changes often or depends on volatile context, compute on the fly.
How many workers are enough for small teams
Begin with Odoo 19 workers & gevent configuration at 4 to 6 workers for modest traffic. Increase based on CPU usage and request queue length.
Best time to run heavy crons
Schedule Cron job scheduling & queue performance in Odoo 19 during off-peak minutes with chunking and checkpoints. Monitor runtime and adjust.
How to avoid regressions after upgrades
Keep a performance test suite that covers critical flows. Use Reduce SQL queries with read_group / prefetch and reverify indexes on any new fields introduced by the upgrade.