High Odoo CPU usage is almost always caused by one of four things: a cron job processing too many records at once, PDF/report generation with wkhtmltopdf, JavaScript asset compilation triggered by a module update, or an inefficient database query in a custom module running a sequential scan on a large table. Identifying which cause applies takes under five minutes with the right tools.
Why Odoo Can Spike CPU
Odoo is Python-based, and Python's CPU consumption is dominated by two activities: executing application logic (loops, ORM calls, report rendering) and waiting on I/O (database queries). When CPU is genuinely saturated — rather than the process simply waiting on PostgreSQL — the cause is almost always in one of these categories:
- Cron jobs: Scheduled actions that process thousands of records in a single call (e.g., sending mass emails, computing stock valuations, syncing with external APIs) can saturate a CPU core for minutes at a time.
- PDF generation:
wkhtmltopdfis a CPU-intensive Chromium-based renderer. Generating 50 invoices in a batch will pin one or more CPU cores. - Asset compilation: When a module is installed or updated, Odoo re-compiles all LESS/SCSS stylesheets and concatenates JavaScript bundles. This is a one-time but very CPU-intensive operation.
- Bad custom code: A custom module with an O(n²) loop over a recordset, or one that calls
sudo().search()with no domain on a large model, can cause continuous CPU spikes. - PostgreSQL CPU: A query doing a sequential scan on
stock_moveormail_messagewill show up as high CPU on thepostgresprocess, notpython3.
Step 1: Identify What Is Consuming CPU
# Interactive process view — press P to sort by CPU
htop
# Non-interactive snapshot — sort by CPU
top -b -n 1 | head -30
# Separate Odoo worker PIDs from PostgreSQL
ps aux --sort=-%cpu | head -20
# Is it Python (Odoo workers) or postgres processes?
ps aux --sort=-%cpu | grep -E "python3|postgres" | head -10
# Load average over 1, 5, 15 minutes — sustained high values indicate CPU saturation
uptime
If python3 processes dominate the CPU list, the issue is in Odoo's application layer (cron, report rendering, custom code). If postgres dominates, the bottleneck is a slow database query — proceed to Step 3.
Step 2: Identify the Specific Odoo Process
# Get the PID of the high-CPU Odoo worker
HIGH_CPU_PID=$(ps aux --sort=-%cpu | grep "[p]ython3" | head -1 | awk '{print $2}')
echo "High CPU PID: $HIGH_CPU_PID"
# Get a Python stack trace from the running process (requires gdb or py-spy)
# Install py-spy: pip install py-spy
sudo py-spy top --pid $HIGH_CPU_PID
# Or get a one-shot stack dump
sudo py-spy dump --pid $HIGH_CPU_PID
py-spy top shows a live flamegraph of which Python functions are consuming CPU in the running Odoo process without requiring any code changes. This is the fastest way to identify a runaway custom module function.
Step 3: Check PostgreSQL for CPU-Heavy Queries
-- Connect to your Odoo database
psql -U odoo -d mydb
-- Find queries currently consuming high CPU (sorting by total time)
SELECT pid,
now() - pg_stat_activity.query_start AS duration,
query,
state,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE state = 'active'
AND (now() - pg_stat_activity.query_start) > interval '2 seconds'
ORDER BY duration DESC;
-- Kill a runaway query (replace PID with the actual PID)
SELECT pg_terminate_backend(12345);
-- Find queries doing sequential scans on large tables
-- (requires pg_stat_statements extension)
SELECT query, calls, mean_exec_time, rows, shared_blks_read
FROM pg_stat_statements
WHERE shared_blks_read > 100000
ORDER BY mean_exec_time DESC
LIMIT 10;
Step 4: Check Odoo Logs for the Source
# Find slow requests in Odoo access log
grep "TIME" /var/log/odoo/odoo.log | awk '{if ($NF > 5000) print}' | tail -20
# Find cron job execution times
grep -i "cron\|scheduler" /var/log/odoo/odoo.log | grep -v DEBUG | tail -30
# Find wkhtmltopdf (PDF renderer) calls
grep -i "wkhtmltopdf\|report" /var/log/odoo/odoo.log | tail -20
# Check for Python exceptions that might indicate a code loop
grep -E "ERROR|Traceback|RecursionError|TimeoutError" /var/log/odoo/odoo.log | tail -30
Fixing High CPU by Cause
Fix 1: Cron Jobs Running Too Frequently or Processing Too Much
# Limit cron worker processes in odoo.conf
max_cron_threads = 1 # Default is 2 — reducing to 1 limits cron parallelism
In the Odoo UI: Settings → Technical → Automation → Scheduled Actions. For any cron job that processes large datasets:
- Increase the interval (e.g., from every hour to every 4 hours)
- Add a
LIMITclause to the domain so each run processes at most N records - Schedule CPU-heavy jobs for overnight hours when user traffic is low
Fix 2: PDF Generation Causing CPU Spikes
# Increase worker time limits to avoid killing PDF workers prematurely
# In odoo.conf:
limit_time_cpu = 120 # Give wkhtmltopdf more CPU time per request
limit_time_real = 300
For bulk invoice printing (hundreds of PDFs), use the Print action with the Generate in background option (available in Odoo 16+) so PDF generation runs as an async job rather than blocking an HTTP worker.
Fix 3: Asset Compilation After Module Updates
# Asset compilation is a one-time event after module install/update
# Force it to complete immediately rather than lazily on first user request
./odoo-bin --addons-path=... -d mydb -u your_module --stop-after-init
# After compilation, clear the browser cache — stale assets cause reload loops
# In Odoo debug mode: Settings → Technical → User Interface → Clear Assets Cache
Fix 4: Identifying and Fixing Bad Custom Code
# SLOW: loads entire recordset into memory, then filters in Python
records = self.env['sale.order'].sudo().search([])
filtered = records.filtered(lambda r: r.amount_total > 1000)
# FAST: push the filter to PostgreSQL
records = self.env['sale.order'].sudo().search([('amount_total', '>', 1000)])
# SLOW: N+1 query problem — one SQL query per iteration
for order in orders:
print(order.partner_id.name) # triggers a new query each time
# FAST: prefetch related records in one query
orders = orders.with_prefetch() # or access partner_id before the loop
Using Worker CPU Limits to Protect the Server
# odoo.conf — hard limits prevent any single request from monopolising CPU
limit_time_cpu = 60 # Kill worker if it uses more than 60 s of CPU
limit_time_real = 120 # Kill worker if the request takes more than 120 s wall-clock
These limits are your last line of defence against runaway requests. If a worker is killed by limit_time_cpu, Odoo logs a warning — treat it as a signal to optimise the code path, not just raise the limit.
How DeployMonkey Handles High CPU
DeployMonkey's monitoring stack tracks CPU usage per Odoo worker process in real time. If any worker sustains above 90% CPU utilisation for more than 60 seconds, the dashboard raises an alert with the worker PID, the last logged request URL, and the duration — giving you the information needed to diagnose the cause immediately.
All instances enforce limit_time_cpu and limit_time_real at provisioning time. Workers that exceed their CPU time budget are automatically recycled, preventing a single bad cron job or report from degrading the entire instance. On Pro and Agency plans, cron execution history is visible in the dashboard with per-job CPU and duration metrics.
For Agency customers managing multiple instances, the platform provides a cross-instance CPU heatmap showing which instances are under sustained CPU pressure, making it easy to identify a client's instance that needs an upgrade before users start complaining about slowness.
Related Articles
- How to Configure Odoo Workers for Best Performance
- Odoo Slow? How to Diagnose and Fix Performance Issues
- How to Optimize PostgreSQL for Odoo
- How to Fix Odoo Out of Memory Error
Frequently Asked Questions
How do I tell if high CPU is coming from Odoo or PostgreSQL?
Run ps aux --sort=-%cpu | grep -E "python3|postgres" | head -10. If python3 (Odoo) is at the top, the bottleneck is application-level — cron jobs, report generation, or custom code. If postgres dominates, the issue is a slow database query that needs indexing or query optimisation.
Can I limit how much CPU a single Odoo cron job uses?
Not directly at the OS level without cgroups. The best approach is to set limit_time_cpu in odoo.conf so that any cron job execution exceeding that CPU budget is automatically terminated. Additionally, reduce max_cron_threads = 1 to limit parallel cron execution.
Why does Odoo spike CPU when I first load a page after a server restart?
On first access after a restart, Odoo compiles JavaScript and CSS assets and warms up the ORM cache. This initial compilation can take 20–60 seconds of high CPU on a server with many installed modules. Subsequent requests are served from cache and are much faster. You can pre-warm assets by running the Odoo CLI with --stop-after-init after deployment.
Does installing more Odoo modules increase CPU usage?
Yes, in two ways. More modules mean more Python code imported at startup (higher startup CPU). More modules also mean more computed fields evaluated on common models, which increases the CPU cost of every ORM read operation. Only install modules that are actively used in your workflows.
What is wkhtmltopdf and why does it use so much CPU?
wkhtmltopdf is the external HTML-to-PDF renderer that Odoo uses to generate invoices, delivery orders, and other printable reports. It internally renders HTML in a Chromium-based engine, which is CPU-intensive. Each PDF generation call spawns a new wkhtmltopdf process. For bulk printing, this can saturate a CPU core for seconds per document.
CPU Alerts and Worker Limits — Built In
DeployMonkey monitors Odoo CPU usage per worker, enforces time limits automatically, and alerts you when any instance sustains high load — before your users notice.
Start Free — No Credit Card Required