Skip to main content
Odoo 20 Performance Optimization: How to Diagnose Slow Views, Reports & Custom Code
12 min read
Odoo

Odoo 20 Performance Optimization: How to Diagnose Slow Views, Reports & Custom Code

Diagnose slow Odoo 20 views, reports and custom modules with profiling, query analysis, batching, index review and measured infrastructure checks.

When Odoo feels slow, the expensive mistake is to start changing infrastructure before proving where the time is going.

A slow Sales list can come from a custom compute method. A report can be dominated by SQL. A search can become expensive only when a particular record rule or filter is active. A cron job can saturate workers even while ordinary requests are healthy. And an upgrade can expose code that was already inefficient but only became visible when data volume or framework behavior changed.

A useful Odoo 20 performance optimization process therefore starts with one reproducible slow action, measures it, identifies whether the bottleneck is SQL, Python, QWeb, an external dependency, or server capacity, and changes one cause at a time.

Odoo 20 includes an integrated profiler specifically for this work. Its official performance documentation describes profiling of executed SQL queries and stack traces, with results viewable through Speedscope. The same documentation recommends batching recordset operations, reducing algorithmic complexity, using prefetch effectively, and adding indexes selectively rather than automatically. See the official Odoo 20 performance documentation.

This guide turns those tools into a practical production workflow.

First, define the slow action precisely

"Odoo is slow" is not a diagnosis.

Start with one operation that can be repeated:

  • opening the Sales Orders list with a known filter;
  • confirming a quotation;
  • loading a large accounting report;
  • validating a stock transfer;
  • opening a custom dashboard;
  • importing a fixed sample of records;
  • running one scheduled action;
  • calling one API endpoint with the same payload.

Record the context around it:

ItemExample
User/roleInventory manager
Active companyUAE Trading LLC
Database copyRecent staging copy
Record volume1.8M stock moves
Filter/domainOpen moves for one warehouse
Expected resultList loads with correct records
BaselineMedian of several repeat runs

Do not benchmark only once. Odoo's own performance documentation warns that cache state, other processes, profiling overhead and blocking calls can change measurements. The goal is not to produce a dramatic screenshot; it is to create a baseline that survives another run.

If the slowdown appeared after an upgrade, compare the same business action on the pre-upgrade and upgraded staging databases where possible. That gives you a regression to investigate instead of a vague impression that "the new version is slower." For upgrade-specific testing, see Octivex's Odoo 20 Upgrade Guide.

Use the Odoo 20 profiler before guessing

Odoo 20's integrated profiler can be enabled from developer tools for web requests or from Python for a specific code section or test. The official 20.0 documentation lists three collectors:

  • SQL Collector for executed database queries and their stack traces;
  • Periodic Collector for sampled Python stack traces;
  • QWeb Collector for template directive execution and queries.

The default profiling setup enables SQL recording and periodic traces. Results can be inspected in Speedscope, including combined views and SQL-focused views. Odoo also notes that profiling itself adds overhead, so use profiling to locate the problem and then re-measure the optimized path without treating profiler timings as the final production benchmark.

What to look for in the SQL view

The SQL-density view is especially useful when one user action triggers many small queries.

Patterns worth investigating include:

  • the same query shape repeated once per record;
  • repeated reads of a relationship that should have been prefetched;
  • searches inside compute methods;
  • repeated security-domain evaluation across a large recordset;
  • a report that performs several similar aggregations independently;
  • query time increasing sharply only when a specific filter or grouping is added.

A hundred individually fast queries can still produce a slow request. Query count is part of latency.

What to look for in the Python traces

If SQL is not consuming most of the request, inspect Python work:

  • nested loops over large recordsets;
  • repeated mapping/filtering over the same data;
  • expensive computed fields;
  • serialization of oversized payloads;
  • external HTTP calls inside a synchronous user request;
  • custom overrides that repeat work already done by the parent method.

The profiler is not there to tell you "buy a bigger server." It should narrow the investigation until a specific code path explains the delay.

N+1 queries are still one of the easiest ways to make Odoo slow

Odoo's official performance guide explicitly recommends batch operations and warns against running SQL-producing methods inside a loop over records.

A typical anti-pattern looks conceptually like this:

python
for order in orders:
    order.line_count = self.env['sale.order.line'].search_count([
        ('order_id', '=', order.id),
    ])

That can issue one count query for every order.

The better pattern is to aggregate for the full recordset and map the result back to each record. In Odoo, _read_group is often suitable for this class of problem.

The same principle applies beyond counts:

  • create records in batches when the framework supports it;
  • browse recordsets together so prefetch can work;
  • avoid calling search methods repeatedly with only one ID changing;
  • prepare lookup dictionaries or sets instead of nested loops over large Python lists.

The objective is not "use fewer lines of code." It is to make the amount of work grow predictably as the dataset grows.

Check prefetch before replacing ORM code with raw SQL

When developers see many queries, the temptation is to bypass the ORM.

Sometimes raw SQL is appropriate. Often the real issue is that the code defeats Odoo's recordset and prefetch behavior.

For example, browsing one record at a time and immediately reading a field can force repeated database access. Browsing the complete set first gives Odoo an opportunity to fetch fields for the recordset together.

Before replacing ORM logic, ask:

  1. Is the method operating on a recordset or one record at a time?
  2. Is the same related field being read repeatedly?
  3. Has the code split a recordset into individual browse() calls unnecessarily?
  4. Can the result be obtained with one grouped operation?
  5. Would a stored field remove an expensive repeated calculation without creating unacceptable recomputation cost?

Raw SQL can remove framework overhead, but it can also bypass ORM behavior, access expectations, computed-field dependencies and maintainability. Use it because measurement shows it is needed, not because it feels faster.

Indexes help only when they match the real query

Odoo 20's performance documentation recommends database indexes for fields that are frequently searched, while warning not to index every field because indexes consume storage and add cost to INSERT, UPDATE and DELETE operations.

That trade-off is why "add an index" should not be the first response to every slow search.

A useful index review starts with the actual query shape:

  • Which columns appear in the filter?
  • Is there a sort or grouping that changes the plan?
  • How selective are the values?
  • Is the table large enough for the planner to prefer an index?
  • Does the problematic query appear frequently enough to justify write overhead?

If you control PostgreSQL directly, PostgreSQL's EXPLAIN documentation explains how to inspect the query plan chosen by the database. EXPLAIN ANALYZE actually executes the query, so use it carefully—especially for statements that can write data. PostgreSQL also documents that timing instrumentation can add overhead and that plans on toy-sized data should not be extrapolated blindly to production-sized tables.

For longer-running observation on infrastructure you control, pg_stat_statements can aggregate planning and execution statistics for normalized SQL statements. Access and configuration depend on the hosting environment; do not assume a managed Odoo platform exposes the same database-level controls as a self-hosted server.

Record rules and domains can be the hidden multiplier

A query that is fast for an administrator can be slow for a restricted user.

That is a signal to examine security and business domains, not a reason to test everything as superuser.

Multi-company rules, warehouse restrictions, salesperson domains and custom record rules can add conditions to searches. The difference becomes significant on large tables when the resulting query is difficult for PostgreSQL to execute efficiently.

Performance testing should therefore include the actual role that reports the problem.

Compare:

  • administrator versus restricted user;
  • one active company versus several active companies;
  • no filter versus the real saved filter;
  • one warehouse versus all permitted warehouses;
  • standard view versus a customized inherited view.

If the slow behavior disappears as administrator, the next question is not "how do we give everyone more permissions?" It is "what access condition changed the workload?"

That keeps performance tuning aligned with security instead of weakening permissions to make screens faster.

Reports need separate diagnosis from ordinary views

A report can be slow for a different reason than the screen that launches it.

For a slow accounting, stock or management report, measure the stages separately where possible:

  1. domain construction;
  2. data aggregation;
  3. currency/company logic;
  4. template rendering;
  5. PDF generation or file serialization;
  6. delivery to the browser.

If the profiler shows SQL dominating the request, optimize the data path. If QWeb rendering dominates, focus on the template and repeated directives. If the expensive part is an external renderer or remote service, changing ORM code may do nothing.

This separation matters because "the report takes 20 seconds" does not tell you whether the database, Python, template or rendering boundary owns those seconds.

Cron jobs can make interactive Odoo feel slow

Scheduled work competes for resources with user requests.

A large synchronization, mass recomputation, document generation task or poorly batched cron can create periods where users experience latency even though the business screen itself has not changed.

When slowness appears only at certain times, correlate it with:

  • scheduled actions;
  • integration imports/exports;
  • backup windows;
  • mass email or notification jobs;
  • inventory recomputations;
  • report generation;
  • long transactions holding locks.

Do not simply move every cron to midnight. Some integrations need timely processing, and moving all heavy work into the same quiet window can create a larger resource spike. Measure duration and concurrency, then decide which jobs can be split, batched, rescheduled or moved to a queue.

Infrastructure matters—but measure it after the application path

A real performance problem can absolutely be caused by insufficient CPU, memory, I/O or worker capacity. The mistake is assuming that infrastructure is the cause before examining the request.

For self-hosted production deployments, Odoo 20's official deployment documentation distinguishes the default multi-threaded server from the multi-processing server intended for production. It also publishes a worker-count rule of thumb based on CPU count, while noting that cron workers consume CPU and that memory sizing depends on the request mix. See the official Odoo 20 deployment documentation.

Treat those numbers as starting guidance, not a universal sizing formula.

A sensible infrastructure review asks:

  • Are workers saturated during the slow window?
  • Is memory pressure causing worker restarts?
  • Is disk I/O the limiting resource?
  • Is the database waiting on locks or storage?
  • Are long cron jobs consuming the available capacity?
  • Does adding capacity improve the measured slow action, or merely hide inefficient code temporarily?

If doubling hardware leaves one report just as slow, the report probably did not need more hardware.

Hosting changes should be the result of evidence

Odoo Online, Odoo.sh and self-hosted environments expose different levels of diagnostic and infrastructure control.

One particularly important limitation is documented by Odoo itself: Odoo Online databases cannot use the integrated profiler. That changes the diagnostic path for a SaaS deployment. On Odoo.sh or self-hosted environments, custom-code and profiling options are broader, while direct PostgreSQL-level tooling depends on what the hosting model exposes.

Do not conclude that Odoo.sh is slow because a forum thread says so, and do not conclude that self-hosting will fix every performance issue. Community discussions regularly contain both experiences: infrastructure complaints and cases where query/index work solved the problem without a hosting move.

Treat those discussions as problem signals—not technical proof.

Performance problems after an Odoo 20 upgrade

An upgrade can expose three different classes of issue:

1. The standard application path changed

A view, report or standard workflow may now execute different framework code or SQL.

2. Custom code is compatible but inefficient

A module can install successfully and still perform badly on the new version because it relies on old assumptions about ORM behavior, view loading or method internals.

3. Production-sized data reveals the issue

The upgraded demo database may look fast while the real database has years of accounting lines, stock moves, messages and attachments.

That is why performance belongs in upgrade acceptance testing rather than post-go-live cleanup. Octivex's Odoo quality assurance guide recommends production-like staging and realistic transaction volumes for exactly this reason.

For a critical workflow, keep a before/after measurement with the same user role, dataset and scenario. A successful upgrade should preserve business correctness and acceptable response time.

A practical Odoo 20 performance checklist

Use this sequence before approving a performance fix:

Reproduce

  • Identify one slow action and exact user role.
  • Record active company, filters and representative data volume.
  • Run it several times and establish a baseline.
  • Check whether time-of-day or scheduled jobs affect it.

Profile

  • Capture SQL and periodic traces where the hosting model allows it.
  • Determine whether SQL, Python, QWeb or external calls dominate.
  • Look for repeated query shapes and N+1 behavior.
  • Reproduce with realistic permissions, not only administrator access.

Fix

  • Batch ORM operations.
  • Preserve recordset prefetch where possible.
  • Reduce avoidable nested-loop complexity.
  • Review computed fields and repeated searches.
  • Add indexes only when the real query justifies them.
  • Review heavy cron jobs and integration concurrency.
  • Change infrastructure only when capacity data supports it.

Verify

  • Re-run the exact baseline scenario.
  • Measure without profiler overhead for the final comparison.
  • Confirm the result under the restricted production role.
  • Run regression tests for business correctness.
  • Check write-heavy workflows if indexes or stored computations changed.
  • Document what changed and why.

The fastest fix is the one that removes the measured bottleneck

Odoo performance tuning works best when it is boring and evidence-driven.

Start with the user action. Measure it. Profile it. Separate database time from Python and rendering time. Fix repeated work before buying capacity. Add indexes only for queries that need them. Test with the permissions and data volume that exist in production. Then measure again.

That process avoids two common failures: spending infrastructure money on inefficient code, and rewriting application logic when the actual constraint is capacity.

If your Odoo database has slow views, long-running reports, post-upgrade regressions or custom modules whose performance degrades with data volume, Octivex Tech's Odoo services include performance tuning, technical audits and upgrade planning built around measured production behavior rather than guesswork.