"Odoo is slow" usually means one of four things: too few workers for the number of users, a PostgreSQL database running on default settings, custom code that makes thousands of queries where ten would do, or scheduled jobs and integrations fighting users for resources. The fix is rarely more hardware. This guide walks through how we diagnose and fix Odoo performance on self-hosted and Odoo.sh deployments: sizing workers and memory, tuning PostgreSQL, using the built-in profiler, and the code patterns that make the biggest difference.

Start with measurement, not guesses

Before changing anything, find out where time goes:

  • Which pages or actions are slow — opening a list view, validating a delivery, posting invoices, website checkout.
  • Server metrics — CPU and memory per worker, load average, swap.
  • Database metrics — slow queries, locks, cache hit ratio, table bloat. The pg_stat_statements extension shows the queries that consume the most total time (PostgreSQL: pg_stat_statements).
  • Odoo's profiler — records every SQL query and the Python stack traces of a request, and displays them as a flame graph (Odoo: performance and profiling).

The profiler can be enabled from developer mode for web requests or used as a context manager in code and tests. Note that Odoo Online databases cannot be profiled; Odoo.sh and on-premise can.

Step 1: Size workers and memory correctly

In production, Odoo should run in multi-processing mode with a pool of worker processes. The Odoo deployment documentation gives rules of thumb (Odoo: deploying on-premise):

  • Maximum workers ≈ (number of CPU cores × 2) + 1.
  • One worker serves roughly 6 concurrent users.
  • About 20% of requests are heavy (around 1 GB of RAM per worker), 80% light (around 150 MB).

The documentation's own example: a server with 4 CPU cores and 60 concurrent users needs about 10 workers in theory, but the CPU caps it at 9, so the configuration uses 8 HTTP workers plus 1 cron worker and roughly 3 GB of RAM for Odoo:

[options]
workers = 8
max_cron_threads = 1
limit_memory_soft = 629145600     ; 600 MB: worker is recycled after the request
limit_memory_hard = 1677721600    ; 1.6 GB: worker is killed immediately
limit_time_cpu = 600
limit_time_real = 1200
limit_request = 8192
proxy_mode = True                 ; behind Nginx or another reverse proxy

Common mistakes: running with workers = 0 (threaded mode, meant for development), setting far more workers than CPU cores, and memory limits so tight that heavy reports are killed mid-request. Put PostgreSQL on its own server or reserve resources for it explicitly once you pass a few dozen concurrent users.

Step 2: Tune PostgreSQL

Out of the box, PostgreSQL is configured for a small machine. A few settings matter most (PostgreSQL: resource consumption):

Setting Starting point Why
shared_buffers ~25% of RAM on a dedicated DB server PostgreSQL's own cache
effective_cache_size 50–75% of RAM Helps the planner choose index scans
work_mem 16–64 MB, measured Memory per sort or hash operation; too high multiplies by connections
maintenance_work_mem 512 MB–2 GB Faster vacuum and index builds
random_page_cost 1.1 on SSD/NVMe Default assumes spinning disks

Also:

  • Keep autovacuum healthy. Odoo tables such as mail_message, stock_move and account_move_line grow quickly. Monitor dead tuples and tune autovacuum per table if needed.
  • Use a connection pooler or cap connections. Each Odoo worker opens database connections; db_maxconn keeps them bounded.
  • Stay current. PostgreSQL 18 added asynchronous I/O with up to 3× faster reads for sequential and bitmap scans, plus skip scans on multicolumn indexes (PostgreSQL 18 release). Check which versions your Odoo release supports before upgrading.

Reliable backups belong in the same conversation as performance; see PostgreSQL backup and disaster recovery.

Step 3: Fix the code — ORM anti-patterns

In most slow Odoo instances we audit, custom modules cause the majority of problems. The usual suspects:

Queries inside loops. Calling search or browse per record turns one query into thousands.

# Slow: one search per order line
for line in order.order_line:
    stock = self.env["stock.quant"].search([("product_id", "=", line.product_id.id)])

# Fast: one grouped query for all products
quants = self.env["stock.quant"]._read_group(
    [("product_id", "in", order.order_line.product_id.ids)],
    groupby=["product_id"], aggregates=["quantity:sum"],
)
qty_by_product = {product.id: qty for product, qty in quants}

Writing records one by one. create accepts a list of values, and write on a recordset updates many records in one statement. Batch imports and synchronizations accordingly.

Unstored computed fields in list views and filters. A computed field shown in a list or used in a domain is evaluated for every record on every load. Store it if it is read far more often than its dependencies change, and declare dependencies precisely.

Missing indexes. Fields used in frequent domains, especially on large tables, need index=True or a custom index. Confirm with EXPLAIN ANALYZE on the real query.

Heavy logic in onchange and constraints. These run interactively; keep them light.

The performance guide also documents assertQueryCount for tests, so you can lock in the number of queries a critical method makes and catch regressions in CI (Odoo: performance).

Step 4: Tame cron jobs and integrations

Scheduled actions and external APIs are a common hidden cause of daytime slowness:

  • Run heavy jobs off-peak and process records in batches with commits between them, so one failure doesn't roll back hours of work.
  • Never call slow external APIs inside a user request if you can avoid it. Queue the work and process it asynchronously; set timeouts and retries. Carrier and payment integrations, like those in Integrating Odoo with Nova Poshta and monobank, are classic examples.
  • Watch for lock contention. Long transactions on stock_quant or sequences can block users validating deliveries or invoices.

Step 5: Archive and clean data

Years of chatter messages, attachments, logs and old records slow down everything. Archive or purge what nobody needs: technical logs, outdated email queues, old attachments moved to object storage. Lighter tables mean faster vacuum, backups and upgrades — the last point matters when you plan your next version jump, see Odoo 17 vs Odoo 18.

A diagnostic checklist

  • Odoo runs in multi-processing mode with workers sized to CPU and users
  • Memory limits allow heavy reports without killing workers
  • PostgreSQL settings tuned for the server, pg_stat_statements enabled
  • Autovacuum keeps up on the largest tables
  • Slow pages profiled with Odoo's profiler
  • No queries inside loops in custom modules; batch create and write
  • Indexes exist for frequent domains on large tables
  • Cron jobs batched and scheduled off-peak; integrations asynchronous
  • Old logs and attachments archived

Symptoms and likely causes

Symptom Likely cause First thing to check
Everything slow at certain hours Cron jobs or imports competing with users Scheduled actions running at that time
One list view slow, others fine Unstored computed field or missing index Profiler on that view, EXPLAIN ANALYZE
Validating deliveries hangs Lock contention on stock tables Long transactions in pg_stat_activity
Random 502 errors Workers killed by memory or time limits Odoo logs for limit_memory_hard or timeouts
Slow after months of use Table bloat, growing mail and log tables Autovacuum stats, table sizes
Website checkout slow External API calls in the request Timing of carrier or payment calls

FAQ

Is Odoo.sh faster than self-hosting? Not inherently. Odoo.sh removes infrastructure work and lets you add workers easily, but slow custom code is slow everywhere.

How many users can one server handle? Following Odoo's rule of thumb, a 4-core server supports roughly 50–60 concurrent users with well-written code. Total named users can be several times higher, since not everyone works at once.

Does upgrading Odoo improve performance? Often, yes — recent versions brought ORM and UI improvements. But an upgrade will not fix inefficient custom modules.

Should we add a read replica? For heavy reporting and BI, yes. Point dashboards and exports at a replica so analysts don't slow down operations.

Where should PostgreSQL run for Odoo? On a separate server or managed database once you have more than a few dozen concurrent users, with low network latency to the Odoo workers.

Sources

  1. Odoo. Deploying Odoo on-premise: workers and memory.
  2. Odoo. Performance and profiling.
  3. PostgreSQL. Resource consumption settings.
  4. PostgreSQL. pg_stat_statements.
  5. PostgreSQL Global Development Group (2025). PostgreSQL 18 released.