本文目前仅提供英文版本。翻译即将推出。
You open Settings → Technical → Scheduled Actions and find a cron job stuck in "Running" state for hours or days. Other crons that should run after it have not. The log shows:
INFO: Starting cron 'Backfill Customer Statistics'
... (no further output for hours)
This is one of the most operationally annoying Odoo bugs because it stops other automation silently. It bites Odoo 17.0/18.0/19.0 with the same root causes.
Quick Fix
Find the stuck cron and forcibly reset it:
-- Find stuck crons
SELECT id, cron_name, state, lastcall, write_date
FROM ir_cron c
JOIN ir_cron_progress p ON p.cron_id = c.id
WHERE p.state = 'running'
AND p.write_date < now() - interval '1 hour';
-- Reset the lock
UPDATE ir_cron_progress
SET state = 'failed', done = 0
WHERE cron_id = <id> AND state = 'running';
Then restart the Odoo workers:
sudo systemctl restart odoo
Why This Happens
Odoo crons are dispatched by cron worker threads. Each running cron acquires an advisory lock in PostgreSQL plus a row in ir_cron_progress with state='running'. If the worker process dies (OOM, crash, network kill) without releasing the lock and updating the row, the cron appears "running" forever even though no process is actually executing.
The five common causes:
- Worker OOM-killed. A long-running cron consuming memory hits the worker limit and gets terminated by the OS. The advisory lock is released by PostgreSQL on connection close, but the
ir_cron_progressrow is never updated. - Network blip between Odoo and PostgreSQL. The connection drops mid-cron. Same outcome — DB-level lock auto-released, app-level state stale.
- Manual
kill -9on the cron worker. Same. - Truly long-running cron. The job is doing legitimate work for 6 hours. Not stuck — just slow. Distinguish via
pg_stat_activity. - Deadlock between the cron and another transaction. PostgreSQL detects and aborts one, but if the abort path missed updating
ir_cron_progress, the row stays "running".
Step-by-Step Diagnosis
1. Check ir_cron_progress.
SELECT c.cron_name, p.state, p.done, p.write_date, p.cron_id
FROM ir_cron_progress p
JOIN ir_cron c ON c.id = p.cron_id
WHERE p.state = 'running';
Note the write_date. If older than an hour for a cron that should run quickly, it is stuck.
2. Confirm no actual worker is doing the work.
SELECT pid, application_name, state, query_start, query
FROM pg_stat_activity
WHERE application_name LIKE '%cron%' AND state = 'active';
Empty result for the stuck cron = no worker is running it. The "running" flag is a lie.
3. Check OS-level processes.
ps -ef | grep odoo-bin | grep -v grep
If the cron worker count matches your --max-cron-threads config, no worker is hung — they are idle. The DB state is stale.
4. Check the log around when the cron started.
grep "Backfill Customer Statistics" /var/log/odoo/odoo.log
Look for the line where it started, then any error or signal afterward. OOM-kills often leave a single "Killed" line.
5. Check OS-level OOM history:
sudo dmesg | grep -i "killed process" | tail
sudo journalctl -k | grep "Out of memory" | tail
A recent kill matches the cron start time → root cause is OOM.
Permanent Fix
For stuck crons right now, reset the row:
UPDATE ir_cron_progress
SET state = 'failed', done = 0
WHERE cron_id = <id> AND state = 'running';
Then restart Odoo so the cron worker re-checks and picks up pending crons fresh.
For OOM-killed crons, raise memory limits or split the work:
# odoo.conf — for the cron worker pool
limit_memory_soft = 4294967296 # 4 GB before recycling
limit_memory_hard = 6442450944 # 6 GB before kill
limit_time_real_cron = 14400 # 4 hours max wallclock per cron
limit_time_cpu = 7200 # 2 hours max CPU per cron
For genuinely long jobs, batch:
@api.model
def cron_backfill_statistics(self, batch_size=1000):
pending = self.search([('stats_done', '=', False)], limit=batch_size)
if not pending:
return
pending._compute_statistics()
pending.write({'stats_done': True})
# Schedule next batch immediately
self.env['ir.cron'].sudo().search([('cron_name', '=', 'Backfill Statistics')]).write({
'nextcall': fields.Datetime.now(),
})
This pattern processes 1000 records per cron run; the cron schedules itself again until done. Each run is short, OOM-resistant, and resumable.
For network/connection issues, harden the cron's transaction:
def cron_backfill(self):
try:
self._do_work()
except Exception:
self.env.cr.rollback()
# Update the cron progress row even on failure
self.env['ir.cron.progress'].search([('cron_id', '=', self.id)]).write({
'state': 'failed',
})
raise
Add a watchdog cron that resets stuck rows automatically:
@api.model
def cron_reset_stuck_progress(self):
self.env.cr.execute("""
UPDATE ir_cron_progress
SET state = 'failed'
WHERE state = 'running'
AND write_date < now() - interval '2 hours'
""")
Schedule this watchdog every 30 minutes. It self-heals stuck states.
How to Prevent It
- Always batch long-running crons. Anything that processes more than 10K records should batch. Eliminates OOM and timeout failure modes.
- Set
limit_time_real_cron. A hard wallclock limit forces termination before infinite hangs. Set to 2x the longest-expected legitimate run. - Monitor
ir_cron_progress. A simple query alert: anystate='running'row older than 2x its expected duration. Catches stuck states immediately. - Memory limits with auto-restart.
limit_memory_hardtriggers a clean restart of the worker before OS OOM-kill. Cleaner failure mode. - Idempotent cron design. Crons should be safely re-runnable. If a cron is killed mid-flight, the next run should pick up where it left off without duplicating work. Use
done=Trueflags or processed-since-X queries. - Watchdog cron. Self-heal stuck states automatically. Costs nothing, prevents permanent stalls.
Related Errors
- Slow list view on > 1M rows — sibling performance issue.
- Database locked during import — adjacent long-running write problem.
- Memory leak in cron worker — different cause of the same OOM symptom.
- Too many PostgreSQL connections — what stuck crons can lead to.
Frequently Asked Questions
Can I disable a stuck cron?
Yes. Set active=False in ir_cron. The watchdog will not fire it, and no further runs will pile up. Re-enable after fixing the root cause.
How long is too long for a cron?
Anything over an hour is suspect. Anything over 4 hours is a problem. Long crons block other automation and amplify the cost of any failure. Batch and shorten.
Why does Odoo not auto-detect stuck crons?
Because Odoo cannot distinguish "running for 8 hours legitimately" from "stuck". The progress row is the only signal. A watchdog cron with a sensible age threshold is the right answer — Odoo could ship one, but does not by default. Build it yourself or install OCA's cron_run_manually family of utilities.
Should I use Celery or another task queue instead?
For very heavy workloads, yes. Odoo's cron is fine for moderate scheduled work; for high-throughput async work (millions of small jobs), a dedicated queue scales better. The OCA queue_job module bridges this nicely — install it, decorate methods with @job, and the queue worker handles execution outside the main Odoo cron.
What is the right priority for ir_cron?
Lower numbers run first when multiple crons are due simultaneously. Set critical maintenance crons (data integrity checks, mail queue) to priority 1; bulk computes to priority 5; analytics rebuild to priority 10. Without explicit priorities, ties resolve by id, which is unpredictable.
Can I see the lock that's stuck?
Yes:
SELECT pid, mode, locktype, relation::regclass, granted
FROM pg_locks
WHERE locktype = 'advisory';
Advisory locks are what crons use. If you see one held forever, it points at the worker that died without cleanup. PostgreSQL releases on connection close, so once the worker is gone the lock should clear automatically — if it does not, you have a deeper bug.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
如何将自定义按钮添加到 Odoo 表单视图 (2026)
将自定义操作按钮添加到 Odoo 19 表单视图:Python 操作方法、视图继承、条件可见性、确认对话框。经过生产测试。
如何在没有 Studio 的情况下在 Odoo 中添加自定义字段 (2026)
通过 Odoo 19 中的自定义模块添加自定义字段:模型继承、视图扩展、计算字段、存储/非存储决策。代码优先,版本控制。
如何使用外部布局在 Odoo 中添加自定义报告
使用 web.external_layout 在 Odoo 19 中构建品牌 PDF 报告:QWeb 模板、paperformat、操作绑定。带有印刷徽标+页脚覆盖。