Why Odoo performance tuning is a whole-stack job
Tuning a self-hosted Odoo means matching its worker, memory, PostgreSQL, reverse proxy and custom code settings to the actual workload, then measuring each change before accepting it. A team that sees pages slow under real users, batch jobs or reports is rarely facing one bad setting. The question this page answers is whether that tuning can stay in-house or whether it is worth bringing in a partner who has done it on comparable systems. The bottleneck is often not the hardware. In many Odoo systems the slow path is a custom module with an inefficient query, a computed field that recalculates too often, or a scheduled action that runs a heavy job during business hours. Adding CPU or memory hides the symptom for a while and does not remove the cause. Before any change to infrastructure, the team should be able to name which request, report or scheduled action is slow and how often it runs. Odoo is built on Python and PostgreSQL, so both layers need attention. The Python application server handles requests and business logic, and its worker and memory limits control how many requests run at once and how large a process can grow. PostgreSQL executes the queries, holds the locks and depends on memory settings and maintenance. A change in one layer can shift load to the other, which is why a tuning plan has to treat the stack as one system. Community and Enterprise editions share the same core architecture, so the tuning principles apply to both. What differs is who controls the settings. On-premise hosting gives your team access to the configuration files, the database server and the proxy. Odoo.sh and Odoo Online limit what the client can change, so the options on those hosts are narrower. This page assumes self-hosting. If your environment is hosted by Odoo, confirm with the vendor which parameters you can adjust before planning any of the work described here. As a first step, record the current worker count, memory limits, PostgreSQL version and configuration, reverse proxy timeouts and the list of scheduled actions with their run times. Confirm these values in your own environment, since they determine which of the later steps apply to you.
Which Odoo and PostgreSQL components decide speed
A slow Odoo instance usually traces back to one of a small number of components, each with its own symptoms and its own settings. Work through these cards in order to match what users see to the layer that is actually constrained before changing anything.
HTTP worker processes
The HTTP worker processes handle interactive requests from users and API clients. When too few are available, requests queue behind one another and pages feel slow at peak hours even though the database is idle. Check the number of workers, the request timeout and the worker logs for long requests. Confirm the parameter names and defaults for your exact Odoo version in its documentation before changing them.
Cron worker and scheduled actions
Cron workers run scheduled actions such as inventory reordering, email queues, and recurring imports. A long or badly scheduled action can hold a cron slot, so interactive pages slow down at the same times the job runs. Review scheduled actions for runtime, frequency and overlap, and confirm the cron worker settings and their defaults in the documentation for your version.
Python application code and custom modules
Core modules and custom Python code run inside each worker, so an inefficient loop or an unbounded search in a custom module can slow every user on that worker. Symptoms include a single screen or report that is slow regardless of load. Profile the request, look for repeated queries per record, and review the code path before adding hardware. Custom modules are usually the first place to look when only one function is slow.
PostgreSQL connections and memory
PostgreSQL limits how many connections it accepts and how much memory each query and maintenance task can use. If the connection limit is reached, Odoo workers wait or fail, and if memory settings are too small, sorts and joins spill to disk. Watch active connections, slow query logs and autovacuum activity. Confirm parameter names and their relationship to the worker count against the PostgreSQL and Odoo documentation for your versions.
Filestore and attachments
Odoo stores attachments and many binary fields in the filestore on disk rather than in the database. Slow reads from a network share or a shared volume can make document-heavy screens and reports slow, and a filestore that is not included in backups leaves restores incomplete. Check the storage type, its latency and its placement relative to the Odoo servers, and confirm how your version handles attachment storage in its documentation.
Reverse proxy, TLS and static assets
The reverse proxy terminates TLS, serves static assets and forwards requests to Odoo. Misconfigured timeouts, buffering or compression can cause slow page loads, failed uploads or dropped connections that look like application faults. Confirm which paths the proxy serves directly and which it passes to Odoo, and check proxy timeouts against the longest expected request. Review the proxy configuration in the Odoo deployment documentation for your version.
Long-polling and live-update channel
Odoo uses a separate channel for live updates such as chat notifications and bus messages, and it holds connections open for long periods. If this channel is not routed correctly through the proxy or is served by the same limited pool as normal requests, notifications lag and users see stale screens. Confirm how your version routes this channel and which proxy settings it requires, and test it under realistic concurrent use.
Scaling decisions: vertical, workers, cron placement and caching
This table compares four scaling choices for a self-hosted Odoo: bigger servers versus more worker processes, scheduled actions in the main process versus a dedicated process, co-located versus separate PostgreSQL, and proxy caching versus application caching. Read each row against your measured workload, because the right option depends on whether your bottleneck is CPU, memory, database load, scheduled jobs or static content delivery.
| Decision | When it fits | Operational cost | What to measure first |
|---|---|---|---|
| Add CPU and RAM to one server | Small user counts where a single host has headroom and the bottleneck is one busy process or a heavy report | Fewer moving parts, but a resize usually means a maintenance window and a restart; one host remains a single point of failure | CPU and memory use under peak load, swap activity, and whether the host saturates before the database does |
| Run multiple worker processes | Many concurrent users with CPU-bound requests, and enough memory to hold the extra processes without swapping | Memory headroom must be planned per worker, limits must be tuned together, and long-running requests can tie up workers; debugging spans several processes | Worker memory per process, request queue length, requests exceeding time limits, and worker recycling frequency |
| Scheduled actions in the main process | Light scheduled work on a small deployment where simplicity matters more than isolation | Cron jobs compete with user requests for resources, and a stuck job can slow the whole instance | Duration and overlap of scheduled actions, and whether user response times rise when jobs run |
| Scheduled actions in a dedicated process | Heavy or frequent jobs such as imports, mail queues or large recomputations that should not affect interactive users | An extra process to deploy, monitor and restart, plus a clear rule for which jobs run where | Which scheduled actions take the longest, their schedule overlap, and response times for users during job windows |
| Co-locate PostgreSQL with Odoo | Small or pilot deployments where a single host is acceptable and operating cost must stay low | Shared CPU, memory and disk contention, and backups must capture both the database and the filestore consistently | Database cache hit behavior, disk latency, and whether database and application processes compete during peaks |
| Separate database host | Larger deployments, strict backup or recovery targets, or when the database needs its own sizing and maintenance window | Network latency becomes part of every query, extra hosts to patch and secure, and replication or backup tooling to design | Query latency between Odoo and PostgreSQL, connection counts, and resource saturation on each host separately |
| Reverse proxy caching | Static assets and cacheable responses that the proxy can serve without changing Odoo business logic | Cache rules must exclude authenticated and session-bound pages, and stale content risks need a clear invalidation approach | Share of requests served from the proxy, cache misses on dynamic pages, and whether any user data leaks across sessions |
| Application caching | Repeated lookups inside Odoo where the cost is in computed fields or server-side queries rather than delivery | Cache invalidation follows Odoo's own rules, so custom code must be reviewed at each version upgrade; debugging stale values takes care | Query counts per request, time spent in computed fields, and whether cached values match fresh results after data changes |
How to tune an Odoo deployment step by step
This sequence moves from measuring the current system to changing it one layer at a time, so each change can be tied to a measured result. The platform engineer and DBA carry most of the work, with business owners confirming which screens and jobs matter. The output is a tuned deployment with a before and after record for every change.
Baseline the current system
Record response times on the screens users depend on most, such as sales orders, stock moves or month-end reports, and capture long requests from the Odoo logs. Pull slow query data from PostgreSQL and note memory and CPU use during the busiest hours of a normal week. The platform engineer runs this with business users who name the screens. Done when you have a dated baseline covering all four sources, and the test scenario is written down step by step so every later measurement repeats it exactly.
Separate interactive load from scheduled action load
Compare user request traffic in the logs with the work done by scheduled actions (cron jobs). Identify which jobs run during business hours, which overlap each other, and which hold database locks for long periods. Worker sizing only makes sense once you know how much capacity interactive traffic needs. The output is a timeline that places user peaks beside job runs. Check that every job has a named owner and an expected run window, confirmed with the people who rely on its results.
Set worker and memory limits
In the Odoo configuration file, set the number of workers and the memory and CPU time limits per worker, based on the process model for your exact Odoo version and the memory available on the server. Read the documentation for that version to confirm how workers, cron threads and longpolling are handled before changing values, because the model can differ between releases. Restart Odoo and run the baseline scenario again. Check that no worker is killed for memory or time limits and that the logs show no new errors.
Tune PostgreSQL connections, memory and autovacuum
Align the database connection limit with the worker count, because each worker holds connections to PostgreSQL. Size memory settings for the server, review autovacuum behavior on the largest tables, and confirm table statistics are current after large imports. The DBA reviews the slow queries from the baseline and records each setting that changed. Check by rerunning the baseline scenario and comparing slow query output and lock wait events. Keep a change only if the same scenario improves without raising memory pressure on the server.
Move or reschedule heavy scheduled actions
Move long scheduled actions to off-peak windows, split large batch jobs into smaller runs, or place background jobs on a separate Odoo process if your deployment design allows it. Ask each job owner to confirm that results are unchanged after the move, especially for reports, reconciliations and stock valuations. Check the peak window in the logs for fewer slow requests, and compare job output for the same date range against figures recorded before the change.
Configure the reverse proxy
Place a reverse proxy in front of Odoo to terminate TLS, compress text responses, serve static assets directly, and set timeouts that fit the longest legitimate request, such as a large report export. Confirm with the documentation for your version that longpolling or websocket traffic is routed to its own handler. Ask the network or platform team to review the proxy configuration. Check that baseline screens load faster, static files no longer reach the Odoo workers, and no valid report times out.
Profile custom code and fix the top offenders
Enable profiling on a staging copy of the database with production-like data, run the baseline scenario, and rank custom modules and methods by time spent. Common causes include computed fields that run per record, loops that issue a query on each pass, and overrides that execute on every write. The developer who wrote each module fixes the highest-cost items first. Check each fix with a before and after measurement on the same scenario, then review the logs for new errors.
Finding slow custom code with profiling
Profiling tells you which Python functions and ORM calls consume the time in a slow action, so you can fix the code that causes it instead of guessing. Start by reproducing the slow action in a copy of the production database, not on production itself. A copy made with Odoo's database duplication, which can neutralize data for test use, gives you realistic record volumes without exposing live data or adding load to the system your users depend on. Record the exact steps, the user or scheduled action involved, and the database size, so you can compare runs before and after each change. Next, enable profiling for the affected request or scheduled action. The options available depend on your Odoo version and how you deploy it, so confirm the supported profiling options and their settings in the documentation for your release before you rely on them. Once a profile is captured, read it from the top down. Look first for the functions with the largest cumulative time, then for the same query or method called hundreds or thousands of times in one request. Separate application time, spent in Python, from database time, spent waiting on PostgreSQL. High database time with many small queries points to ORM access patterns. High application time with few queries points to heavy computation or large in-memory structures.
Three patterns account for most slow custom code. The first is the N+1 query pattern: a loop reads one record, then runs a search or reads a related field for each record in that loop. Each individual query is cheap, but together they dominate the request. The second is a stored computed field with broad dependencies. When a field is stored and depends on many related fields or on large related models, Odoo recalculates it for every affected record whenever any dependency changes, which can turn a simple save into a long batch of writes. The third is a report or method that loads every matching record into memory and filters afterward, rather than filtering in the database first. Profiling shows these as repeated query calls, high time inside compute methods, and large record sets created early in the call stack. The fixes belong to the custom module or the third-party add-on, so the work is usually code review plus a targeted change: batch reads, move filters into the search domain, reduce dependencies, or make a field non-stored where its value is only needed on screen. Each change should be re-profiled against the same copy so you can show the improvement and confirm nothing else moved. Keep these changes small and documented, because the same code must be ported at each version upgrade.
Profiling adds overhead, and it can expose operational data such as queries, parameters and stack detail. For that reason it must run only on a non-production environment, and any profiling tool, debug setting or extra logging you enabled must be disabled afterward. Confirm that the production configuration matches its pre-test state, and record the change in your change log. Ask your team or partner to confirm, in writing, which profiling options they used, on which version, and where the captured output was stored and when it was deleted. For a decision-maker, the practical questions are who owns the profiling work, whether it is done in-house or by a partner, and what it costs to repeat after each upgrade. Profiling is a one-time investigation per slow action, but new custom code and each version upgrade can reintroduce the same patterns, so plan for periodic re-profiling as part of support. When you request a quote for this work, ask for the list of slow actions covered, the environment used for reproduction, the profiling method, the fixes proposed, and the verification results before and after each fix.
What drives tuning effort and how to keep it down
This table lists the factors that make Odoo performance tuning longer or costlier, with a practical way to reduce each one. Read each row as a lever: the driver on the left is what adds work, and the right column is what your team can do before a partner is engaged. No effort figure is given here; the scope should be set in writing from a measured baseline.
| Driver | Why it changes effort | How to keep it down |
|---|---|---|
| Concurrent users and peak hours | Tuning worker counts, limits and proxy settings depends on how many sessions overlap at peak, and each peak pattern needs its own measurement. | Record concurrent sessions by hour for several weeks before tuning. Share the peak profile in the scope so it is sized once, not guessed. |
| Database size and table growth | Large tables with heavy writes (accounting lines, stock moves, mail and logs) change query plans and vacuum behavior, which lengthens investigation. | Set a retention policy for logs and old mail. Track table sizes monthly so growth is visible before it slows queries. |
| Custom modules and third-party add-ons | Each custom or community module can add slow code paths, extra queries and version-specific patches, so every one must be profiled and reviewed. | Keep an inventory of installed modules with owner and purpose. Retire unused add-ons and document which custom code is performance-sensitive. |
| Scheduled actions and integrations | Cron jobs, sync jobs and API calls that write to the database compete with users for workers and locks, often at the same hours. | List every scheduled action and integration with its schedule and expected run time. Stagger heavy jobs and move bulk syncs away from business hours. |
| Number of environments (test, staging, production) | Settings must match across environments for test results to mean anything, so each change is repeated and checked in every copy. | Keep worker, memory, PostgreSQL and proxy settings in version control with one source of truth. Use neutralized database copies for test runs. |
| Reporting and PDF generation load | Large reports and PDF rendering use significant memory and CPU for long periods, which can starve interactive users if run during the day. | Schedule large reports and exports for off-peak windows. Limit report size where possible and measure rendering time for the heaviest templates. |
| Compliance limits on profiling data and logs | If profiling output, query logs or user-identifying log lines cannot leave a region or system, collection and analysis must happen on controlled hosts and take longer. | Confirm with your compliance owner what may be collected and where it may be stored before profiling starts. Agree a redaction approach for logs in writing. |
| Missing baseline metrics | Without measured response times, worker utilization and database statistics, every change becomes a trial-and-error cycle. | Capture baseline metrics before any change: response times for key screens, worker busy time, PostgreSQL slow queries and memory use. Confirm the measurement period with your team. |
How tuning changes by deployment model
On a single node, Odoo, its workers, PostgreSQL, the filestore and the reverse proxy all share one server's CPU, memory and disk. This is the simplest layout to tune because there is one place to look when something is slow, but every component competes for the same resources. A large PostgreSQL cache can starve Odoo workers of memory, and a heavy scheduled action can slow interactive users at the same time.
Check these points on a single node:
- Memory budget: Divide available RAM across Odoo worker processes, PostgreSQL shared buffers and the operating system cache before setting any worker count.
- Disk contention: Keep the PostgreSQL data directory and the filestore on storage that can handle both database writes and attachment reads without queuing.
- Backup impact: Confirm that backup jobs run at a time when they will not compete with business hours on the same disks and CPU.
- Scheduled actions: Find which cron jobs run heavy queries and move them away from peak usage windows.
- Headroom: Confirm how much capacity remains for a month-end close or a bulk import, since a single node has no second server to absorb a spike.
Before you change any setting, measure baseline CPU, memory and PostgreSQL activity under a realistic user load, and confirm the exact worker and memory parameters with the Odoo version you run.
With PostgreSQL on its own machine, the database gets a dedicated memory and disk budget, and Odoo servers no longer compete with it for RAM. This often suits databases with large histories or heavy reporting. The trade-off is that every query now crosses the network, and backups, restores and patching must be coordinated between two systems rather than one.
Check these points with a separate database host:
- Network path: Measure round-trip latency between the Odoo servers and the database host, because many small queries amplify even a modest delay.
- Connection limits: Set the PostgreSQL connection limit to match the total worker count across all Odoo processes, plus room for administrative sessions.
- Memory on the database host: Size shared buffers and the operating system cache for the database alone, and confirm this with the PostgreSQL version you run.
- Backup coordination: Confirm that the database backup and the filestore backup are taken in a consistent way, so a restore brings back matching data and attachments.
- Access control: Restrict database access to the Odoo application hosts only, and confirm the authentication method in the PostgreSQL configuration.
Before you commit to this layout, ask for latency and throughput measurements from your own network, and confirm the restore procedure with a test restore of both the database and the filestore.
When several Odoo processes or servers share one PostgreSQL database, the load balancer spreads requests across them. Every node must run the same Odoo version, the same custom modules and the same configuration, or users will see different behavior depending on which node serves them. Because all nodes share one database, the database becomes the central limit on throughput.
Check these points in a multi-node design:
- Consistent configuration: Deploy one configuration source to every node so worker counts, memory limits and module lists do not drift apart.
- Shared filestore: Confirm how attachments are shared between nodes, since each node must read the same files. Verify this against the Odoo high availability design before you choose storage.
- Session routing: Decide whether requests need sticky sessions or whether the deployment can serve any request from any node. Confirm this with the Odoo high availability guidance for your version.
- Long-polling placement: Confirm where long-polling connections are routed, because they are long-lived and can exhaust worker capacity if they land on the wrong backend.
- Database connection pooling: Count total connections across all nodes before raising any per-node worker setting.
Before scaling out, confirm the routing and filestore design in writing against the Odoo high availability documentation for your version, and test a node failure in a non-production environment.
In a container platform, the orchestrator often sets a memory limit for each Odoo container. Odoo's worker and memory settings then operate inside that boundary, and the two can conflict. If the orchestrator kills a container for exceeding its limit, the cause is often a worker setting that assumed more memory than the container was given.
Check these points in a containerized deployment:
- Two memory limits: Confirm the memory limit at the orchestrator level and the memory limit configured in Odoo, and make sure the Odoo value fits inside the container limit with room for each process.
- Worker sizing: Calculate the worker count from the container limit, not from the host machine's memory.
- Scaling behavior: Confirm whether the orchestrator scales Odoo pods horizontally, and whether each new pod is compatible with the shared filestore and database connection limits.
- Restart and health checks: Confirm that health checks do not restart a container while it is still running a long scheduled action.
- Image contents: Confirm which profiling and monitoring tools are included in the image, since adding them later changes the image you test and deploy.
Before you go live, run a load test against the exact resource limits you will deploy, and confirm both memory settings in the manifest and in the Odoo configuration file.
In an air-gapped network, Odoo and PostgreSQL run without direct internet access, so every package, module, profiling tool and monitoring agent must be brought in through an approved transfer process. This changes how you install and update tuning tools, and it can delay adoption of a tool you planned to use. Tuning decisions also depend on what your monitoring can observe inside the isolated network.
Check these points in an air-gapped deployment:
- Tool transfer: Plan how profiling tools and monitoring agents will be packaged, verified and imported, and confirm the approval steps with your security team.
- Version pinning: Pin the versions of each tool and agent you install, so the offline copy matches what you tested.
- Local monitoring: Confirm that dashboards and metrics storage run inside the isolated network and do not depend on external services.
- Source installation: Confirm how Odoo itself will be delivered, whether through a packaged installer or from source, and confirm which dependencies must be mirrored internally.
- Update cadence: Plan how security updates and tool updates reach the isolated network, since each one needs the same transfer process.
Before you commit to a tool, ask the vendor or project for its offline installation method and confirm it works with your operating system and the Odoo version you run.
Securing the tuning work itself
Performance work touches the parts of Odoo that carry the most sensitive data and the most powerful controls, from profiling endpoints to production database copies. The cards below cover the places where tuning changes can quietly open a security gap, and the checks that keep them closed.
Restrict profiling and debug endpoints
Profiling and debug tooling helps find slow code, but in production it can expose request details, internal paths and timing data to anyone who can reach it. Confirm which profiling or debug features are enabled on each environment, and remove or restrict access to them in production. Check that the reverse proxy blocks these paths for external traffic, and that access is limited to named engineers during a supervised tuning window.
Protect the master password during copies
Copying a production database to a test server for tuning means the master password and database manager must stay protected on both sides. The database manager can create, back up, restore and drop databases, so it should not be reachable from the internet or left enabled without a strong master password. Confirm the master password is set, stored in a managed secret, and rotated after the test copy is no longer needed.
Neutralize copies used for profiling
A test copy used for load or profiling can send real emails, trigger payment gateways, post to accounting integrations and run scheduled actions against live partners. Duplicate the database with neutralization so outgoing mail servers, payment providers, cron jobs and external integration settings are disabled or pointed at sinks. Verify the neutralized copy before any load test starts, and document which integrations were checked.
Limit access to logs and profiling output
Odoo logs and profiling output can contain customer names, email addresses, invoice details and record identifiers, often in plain text. Treat these files as personal data: restrict read access to the people who need them, set retention limits, and avoid copying them to shared drives or chat channels. Check that log levels used during tuning do not capture request bodies or full SQL parameters unless there is a documented reason.
Keep credentials out of shared files
Tuning typically involves database credentials, proxy settings, SMTP details and cache connection strings, which often end up in shared configuration files or tickets. Move these values into a managed secrets store, reference them from the Odoo configuration at runtime, and remove plaintext copies from working directories. Confirm that rotated credentials are picked up by all workers and cron processes without leaving old values behind.
Version and review configuration changes
Worker counts, memory limits, PostgreSQL settings and proxy directives change the behavior of the whole system, so they should be tracked like code. Keep each configuration change in version control with a written reason, a measured baseline and a second reviewer. Record which environment each change was tested on, so a rollback is a known action rather than a search through server history.
How to verify the tuned Odoo handles load and fails safely
The tuning is only finished when a test shows it works. Before anyone signs off, rerun the same baseline scenarios that were captured before changes, this time against the tuned staging system loaded with a realistic copy of production data. Odoo's database duplication with neutralization is the usual way to build that copy, so outgoing email, payment connectors and external API calls do not reach real parties during testing. Compare response times and error rates for the same user actions side by side with the baseline. The comparison should cover the workers, memory limits and PostgreSQL settings that were changed, not only the average page time, because a single fast page can hide slow list views or report generation that users hit every day.
Next, run a peak-load test that mixes the workload the system really carries. Interactive users working in sales, inventory and accounting screens should run alongside heavy report generation and the scheduled actions that fire at busy times, such as stock valuation, reminder emails and automated invoicing. A test that only logs in many users and loads the home page will not exercise the long-running requests that tie up workers. Watch the PostgreSQL connection count, lock waits and slow query log during the run. Confirm that when the load exceeds what the system can serve, requests queue or receive a clear error, and that the database stays responsive and does not run out of connections or memory. Predictable degradation is the outcome to look for.
Failure tests come next, and each one should have a stated expected result. Restart one Odoo worker while load is running and confirm that in-flight requests fail cleanly, that the worker returns to the pool, and that users can retry without corrupted records. Send a test request that exceeds the memory limit and confirm that the worker is recycled by the configured limit rather than growing until the host swaps or fails. Cut the database connection briefly, for example by stopping the PostgreSQL service or blocking the port for a short period, and confirm that Odoo reconnects and that users see an error rather than stale or partial saves. Each of these tests belongs in the staging runbook with the commands used, so that the same test can be repeated after later upgrades.
Scheduled actions need a specific check. After the database connection or a worker is interrupted, confirm that the scheduled actions resume and that none of them process the same records twice. Duplicate invoices, duplicate emails or double stock moves are the costly failure here. Check the scheduled action logs and the affected records against a known list of expected items, and confirm that any locking or idempotency logic in custom code behaves as designed. If a custom module runs its own cron logic, ask the developer to show how it handles a restart midway through a batch. This check is easy to skip and expensive to miss.
A test passes only when the measured criteria agreed in advance are met. Agree the thresholds for response time, error rate, memory use and recovery behavior before the first run, and write them into the test plan with the people who will accept the result. Record each run with the configuration version, data set, load profile, observed results and any deviations, then keep those records with the handover documents so the operations team can repeat the tests after a version upgrade or a configuration change. Ask your implementation partner to confirm the exact threshold values and the reporting format in writing as part of the scope, since these depend on your own hardware, user numbers and business hours.
What to monitor, document and revisit after tuning
Tuning is only as good as the evidence behind it and the checks that show it still holds. The cards below cover what to watch in production, how to record each setting and its justification, and when to repeat the baseline after changes to Odoo, the data or the environment.
Response times and worker utilization
Track request response times by route, such as sales orders, invoicing and portal pages, alongside how busy each Odoo worker is over the business day. A rising queue at peak hours usually means the worker count or the request timeout no longer fits the load. Compare these figures against the baseline you captured before tuning, not against an idealized target.
Memory use per process
Record resident memory for each Odoo worker and for cron processes, then watch how it grows over days and after large imports or reports. Limits set for memory per worker only protect the server if the measured footprint stays below them under real workload. Confirm which processes your deployment starts and whether each has its own limit before you read the numbers.
PostgreSQL slow queries and bloat
Enable slow query logging or a statistics extension, review the most expensive statements each week, and check table and index bloat on the largest models. Slow queries often come from custom code or missing indexes rather than from database settings, so note which module issued each one. Bloat that grows after heavy write activity may call for a maintenance schedule that your DBA agrees to run.
Scheduled action duration and failures
Measure how long each scheduled action runs and whether it completes, retries or fails. Long-running crons can hold a worker, delay other jobs and push response times up for interactive users. Review failures in the server log after each module update, because a changed dependency can break a job without any visible error in the user interface.
Reverse proxy errors and timeouts
Watch the reverse proxy access and error logs for 502 and 504 responses, upstream connection failures and long-polling errors. These often appear when the proxy timeout is shorter than the longest Odoo request, or when workers restart under memory pressure. Correlate proxy errors with worker logs so you can tell a proxy setting problem from an application problem.
Settings register and justification
Keep a register that lists each tuned value, such as worker count, memory limits, time limits, PostgreSQL parameters and proxy timeouts, with the reason it was chosen and the test that justified it. Store the register in version control next to the deployment code so every change has an owner and a date. Without it, the next engineer cannot tell a deliberate setting from a leftover experiment.
Baseline re-runs after changes
Repeat the baseline load test after every Odoo version upgrade, every significant module update and any sustained growth in data volume, such as a new company or several years of archived records. Use the same scenarios and dataset shape as the original test so the results can be compared. Update the settings register when a re-run shows that a value no longer fits.
Reviews when the environment changes
Review the tuning when the upgrade service, support plan or hosting arrangement changes. Odoo major version upgrades on Odoo Online and Odoo.sh are mandatory every two years, and on-premise environments follow your own upgrade schedule within standard support. Each of these events can change the code, the database or the server layout that the current settings were built around.
Odoo performance tuning questions buyers ask
How much does Odoo performance tuning cost for a self-hosted system?
Cost depends on the written scope, so no single figure applies to every self-hosted Odoo system. The main drivers are how much baseline measurement you want, how much custom code must be reviewed, how many environments need changes, how many performance tests must be written and rerun, whether PostgreSQL configuration and query work is included, and whether profiling of custom modules is in scope. Most of the spend is skilled labor for measurement, changes and retesting. Your hosting infrastructure is a separate cost. Ask for a fixed-scope quote with each item listed as a written deliverable, and ask which parts are one-time work and which recur, such as re-testing after an upgrade or a monitoring retainer.
How long does an Odoo performance tuning project usually take?
The duration depends on the baseline you start from and on the defects the measurements reveal, so no reliable number exists before those are known. A project that finds only settings changes moves faster than one that uncovers slow custom code, missing indexes or database maintenance problems across several environments. Ask the partner for a phased plan: a baseline and assessment phase that ends with written findings, a change phase with agreed settings, and a verification phase that reruns the same test scenario. Each phase should state its exit criteria and when the plan will be revised if new defects appear. Also ask how production change windows will be scheduled, since approvals often take longer than the technical work.
What does the client need to provide for Odoo tuning work?
The client provides access, data and decisions, and the partner provides the analysis and changes. You will need a production-like copy of the database and filestore, handled under your own data protection policies, and a metrics history from the application, the database and the reverse proxy that covers busy periods. Name an owner for each business-critical screen, such as order entry, month-end reporting or warehouse picking, so the test scenario reflects actual work. Agree a change window for staging and production, and name the person who can approve settings changes. Without the metrics history and named owners, the team can only guess which workflows matter, which adds time to the assessment.
Can an in-house team tune Odoo performance without a partner?
Yes, an in-house team can do this if it has PostgreSQL and Python skills, a repeatable test scenario and a settings register. PostgreSQL skills cover query plans, indexes, vacuum behavior and connection handling. Python skills are needed to read profiling output from custom modules and to change code safely. A repeatable test scenario lets you run the same workflow before and after each change, so the results are comparable. A settings register records each worker, memory, PostgreSQL and proxy value, who changed it, why, and what effect was measured. Teams without one of these usually lose time in the first measurement cycle. A partner can also be engaged for a one-time assessment, scoped separately from ongoing work.
Will performance tuning fix an Odoo system that is slow by design?
Often it will not. Tuning adjusts how the system uses the hardware and database it already has, so it cannot remove work that the design requires on every page load. Typical examples include a report that recalculates thousands of records on each click, a workflow that triggers many scheduled actions in sequence, or an approval process that adds many steps to every order. Those problems usually need changes to custom modules, a different data model or a revised business process. Ask the assessment to classify each slow workflow as settings, database, code or design, and to state which categories are in scope. Illustrative: if one custom report causes most of the load, rewriting that report usually helps more than adding workers.
How do I find slow custom code in an Odoo system?
Start with a repeatable test scenario on a staging copy, then profile the requests it triggers and sort the results by cumulative time. Use the PostgreSQL slow query log to link expensive queries to the module that issues them. Look for ORM calls inside loops, which often produce many small queries for one record set, and for computed fields that recalculate on every read. Check scheduled actions that run during business hours and compete with users for workers. Confirm each fix by rerunning the same scenario and comparing timings against the baseline. Record the findings in the settings register or a code findings list, with the module, the method and the measured effect, so the same problem is not reintroduced during the next upgrade.
What should an Odoo performance tuning quote include?
A quote should list the scope as written deliverables, so each item can be checked at handover. Expect a baseline with its metrics, the list of workflows tested, the environments covered, a settings register, the PostgreSQL changes and their maintenance plan, profiling of named custom modules or an explicit exclusion, a verification run against the same scenario, a findings report and a knowledge transfer session. Ask what is excluded, such as fixes to custom code found during profiling, third-party modules, hosting changes and re-testing after each Odoo upgrade. Ask how changes found after the assessment are priced and which deliverables trigger each payment. DeployOpen typically works on fixed-scope engagements, with scope and deliverables agreed in writing before work starts, and a quote follows from that written scope.
How DeployOpen scopes, tunes and hands over Odoo performance work
DeployOpen approaches Odoo performance tuning and workers as a fixed-scope piece of work inside your own infrastructure, starting from a written baseline of how the system behaves today. Changes are made to worker, database, proxy and custom code settings in a test copy first, then moved to production with documentation your team can run. Ongoing support is available as a retainer.
How we scope
- Confirm the Odoo version, edition and hosting so tuning targets the platform you actually run.
- Record the written scope: which workers, database settings, proxy rules and slow modules are in or out.
- Agree the test environment and how production data is copied, neutralized and refreshed for measurement.
- Set out the acceptance checks and the evidence you will receive before work starts.
How we build
- Capture a baseline of worker load, memory use, PostgreSQL activity and slow requests before changing anything.
- Adjust worker, memory and cron limits one group at a time, recording the result of each change.
- Profile suspected slow custom code and report the findings with the functions and queries responsible.
- Test the reverse proxy and caching configuration against real user paths in the test environment before production.
How we hand over
- Deliver a configuration record listing each changed setting, its previous value and the reason for the change.
- Document scheduled actions and their timing so operators can see which jobs compete for resources.
- Run a walkthrough with your administrators on how to repeat the baseline and check performance after a change.
- Provide a list of open findings, including custom code that needs developer work before it can be tuned further.
How we support
- Keep performance changes compatible with the Odoo version you run and its standard support window.
- Re-check worker and database settings after each planned Odoo upgrade, using the upgraded test database first.
- Offer ongoing support and continuous review as a retainer, scoped in writing like the original work.
- Confirm with your team which metrics to monitor and who acts on alerts between engagements.

