A scheduled action’s fields all look right — active is on, nextcall is in the past, numbercall isn’t zero. It still never runs. Every guide about ir.cron walks through the model’s own fields, but a job whose configuration is perfect can still never fire for reasons that have nothing to do with the job itself: the server simply never looks for due jobs in the first place.
The short version: before debugging a specific scheduled action's own fields, rule out two server-level causes that produce the identical symptom — no dedicated capacity to run cron at all (--max-cron-threads), and a scheduler that hasn't started polling yet because no request has reached the server. Neither shows up by inspecting the ir.cron record itself.
Cause 1: no thread is actually dedicated to running cron
Per Odoo’s own CLI reference, --max-cron-threads sets the number of workers dedicated specifically to processing cron jobs — threads in multi-threading (development) mode, separate worker processes in multi-processing (production --workers) mode. The documented default is 2.
That number is a hard capacity limit, not a suggestion: if it’s explicitly set to 0 in a deployment’s configuration — a Docker Compose file, a systemd unit, a .conf copied from a template and trimmed down for a “lightweight” environment — there is no thread whose job is ever to check for due scheduled actions. Every ir.cron record can have flawless fields and it will never matter, because nothing is ever polling them.
Watch out: this is easy to introduce by accident. A minimal-looking config file trimmed down from a real one, or a container image built for a "worker-less" staging environment, can drop --max-cron-threads (or the equivalent max_cron_threads config key) to 0 with no error at startup — the server runs completely normally, serves requests fine, and simply never executes a single scheduled action, silently, forever.
Checking this takes one line in the actual startup configuration (command-line flags or the .conf file’s max_cron_threads key) — not the ir.cron UI, which has no visibility into server-level thread allocation at all.
Cause 2: the scheduler hasn’t woken up yet
A separate, well-documented quirk reported repeatedly in Odoo’s own community forum: in some deployment configurations, the cron scheduler only actually begins polling for due jobs after the server has received its first HTTP request — a server that’s started but never been visited (a freshly deployed staging box nobody’s opened yet, a container health check that only pings a lightweight endpoint) can sit indefinitely with scheduled actions that are all correctly configured and simply never triggered, because the polling loop itself never got its first kick.
The practical fixes reported for this: explicitly setting db_name in the server configuration so the target database initializes on startup rather than waiting for a request to select one, or — as a pragmatic workaround some deployments still use — an external uptime check that hits any page on the instance shortly after every restart, which incidentally also wakes the scheduler.
Checking which of the two it actually is
Neither cause requires touching the database — both are visible from the server’s own process and logs:
- Docker Compose:
docker compose logs <odoo-service-name> | grep -i cron— a healthy setup logs the cron worker starting up shortly after the container itself starts; total silence for “cron” points at cause 1. - systemd:
journalctl -u <odoo-service-name> --since "1 hour ago" | grep -i cron— same idea, reading from the unit’s own journal instead of a container log. - Either way, confirm the actual startup flags first:
ps aux | grep odoo(or the container’s own entrypoint command) shows every flag the process was actually launched with, including whatever--max-cron-threadsvalue really is — more reliable than trusting a.conffile nobody’s re-read since it was written, since a command-line flag silently overrides it.
If the logs show the cron worker starting and picking up jobs at all, both server-level causes are ruled out — the remaining suspects are entirely at the job’s own configuration level, covered next.
Only after ruling out both: check the job itself
Once server-level capacity and wake-up are confirmed fine, the usual ir.cron-level suspects are exactly what the ir.cron Scheduler Helper is built to walk through: whether numbercall has actually been exhausted (0 means no runs left), what doall does to a backlog of missed runs after downtime, and how nextcall actually advances after each execution. That tool assumes the server is polling at all — which is precisely the assumption this article exists to make sure is actually true first.
Where to actually look: logs, not the UI
None of the above shows up by inspecting a scheduled action’s form view — that view only reflects the record’s own fields, with no awareness of whether any thread is polling it. The server’s own log output is the only place that reflects reality: a healthy cron thread logs starting and finishing each due job by name; a server with --max-cron-threads=0 or one still waiting for its first request logs nothing about cron at all, which is itself the tell — silence in the logs, not an error message, is the actual symptom of both causes in this article.
Quick reference
| Symptom | Likely cause | Where to check |
|---|---|---|
| No scheduled action has ever run, logs show zero cron activity | --max-cron-threads is 0 | Server startup configuration/flags |
| Fresh deployment, never manually visited, nothing has run since | Scheduler hasn’t received its first request yet | db_name config, or just load any page once |
Logs show the job starting and failing, or numbercall is 0 | The job’s own configuration | The ir.cron Scheduler Helper |
| Some jobs run, one specific job doesn’t | Almost certainly the job’s own fields, not server-level | The ir.cron Scheduler Helper |
Frequently asked questions
How do I check the current value of --max-cron-threads on a running server?
It's a startup-time setting, not something stored in the database — check the actual command line the server was launched with, or the max_cron_threads key in whatever .conf file it was pointed at. There's no in-app setting that reflects it.
If some scheduled actions run and others don't, is it still a server-level issue?
No — if the server is running cron jobs at all, thread capacity and scheduler wake-up are both clearly working. A single job failing while others succeed is a job-level problem (its own domain/model/code, or its numbercall/active fields), not a server-level one.
Does increasing --max-cron-threads make jobs run faster or more often?
It increases how many cron jobs can execute concurrently when several are due at once — it has no effect on any individual job's own schedule (its interval_number/interval_type/nextcall), only on whether they queue up behind each other or run in parallel.
Is this different in multi-worker (--workers > 0) production deployments?
The mechanism is conceptually the same — --max-cron-threads still sets dedicated cron capacity, just as separate worker processes rather than threads within one process — but a misconfigured multi-worker deployment is exactly where a config trimmed down for "just the web workers" is most likely to accidentally zero out cron capacity, since it's a separate flag from --workers that's easy to overlook.
Does any of this apply to Odoo Online or Odoo.sh?
No — both are managed hosting where Odoo itself controls server startup flags and infrastructure, so neither server-level cause in this article is something a customer can misconfigure. This article is specifically about self-hosted/on-premise deployments, where the person running the server also owns its startup configuration.
Further reading
- Command-line interface (CLI) — official Odoo 18.0 documentation, for the exact, current set of server startup flags including
--max-cron-threads. - —max-cron-threads — Odoo Development docs, a focused reference on this specific flag.
- The Odoo community forum thread documenting the first-request wake-up behavior and the
db_nameworkaround.
And on this site: the ir.cron Scheduler Helper for everything at the job-configuration level, once the server itself is confirmed to actually be polling.