The scheduler does not run your job. It puts the job on a queue, and a worker runs it. Three separate things can be off: the scheduler, the worker for that queue, and the job's own registration.
Pick what you see below, and the module walks the seven checks, each with the command that tests it.
Try it
What do you see?
Pick the symptom. The seven checks come in the order that fits it.
What do you see?
Is the job registered as a Scheduled Job Type?Start here
Open Scheduled Job Type and search for the function's dotted pathIf not bench --site SITE migrate. Check the path imports: migrate prints a warning for one that does not.
Is the site's scheduler switched on?
Run If not bench --site SITE scheduler enable, then scheduler resume. Turn maintenance mode off.
Is a scheduler process running?
Run If not Start bench schedule, or its supervisor program or container service.
Is the job Stopped?
Open Scheduled Job Type → StoppedIf not Untick Stopped.
Is it due?
Run If not Read Last Execution and the cron line. The trigger command runs the job now, to prove the code.
Is a worker listening to its queue?
Run If not Start a worker for default and long: bench worker --queue long. A full queue refuses new jobs.
Did it fail or run past its timeout?
Open Scheduled Job Log (status, details) and Error LogIf not Fix the exception, or move the job to a Long frequency (1,500 s instead of 300 s).
Frappe v16.52.0; the same frequencies, queues and timeouts hold on v15.123.0. SITE is your site’s name.
How does a line in hooks.py become a running job?
You add a function to scheduler_events in hooks.py. Nothing happens until bench migrate, which turns each entry into a row in Scheduled Job Type. The scheduler is one process, started by bench schedule or by its supervisor or container service. It holds a file lock so that only one runs, and it wakes every four minutes, aligned to the clock.
At each wake it skips a site that is in maintenance mode, paused, disabled, or has Enable Scheduler unticked in System Settings. For every job that is not stopped and is due, it puts a message on a queue. A worker takes it, runs the function, commits if it finished and rolls back if it raised. Jobs on every frequency except All and Cron also write a Scheduled Job Log row; those two write one only if Create Log is ticked.
Which queue, and how long does the job get?
Frappe has three queues: short and default allow 300 seconds, long allows 1,500. Scheduled jobs go to default, except frequencies called Long or Maintenance, which go to long. The "All" frequency runs every few minutes, set by scheduler_interval (240 seconds, so every four, by default). A job is not queued again while its earlier run is still waiting in the queue.
A queue refuses new jobs once it holds 500, plus 50 per site (max_queued_jobs changes the 500). A job that hits a deadlock or a lock timeout is retried up to five times. ERPNext's own jobs show the pattern: stock reposting on a 15-minute cron line, reminders hourly, most daily work under daily_maintenance, and deferred revenue under monthly_long.
Why did my job not run?
Seven checks, in the order that finds the cause fastest. bench doctor shows the scheduler and the workers at a glance.
- Is the site's scheduler on?
bench --site SITE scheduler status. - Is a scheduler process running?
- Is the job registered? Open Scheduled Job Type and search for the function. If it is missing, you changed
hooks.pyand did not migrate, or the dotted path is wrong: migrate prints a warning for a path it cannot import. - Is it Stopped?
- Is it due? Last Execution and the cron line say.
bench --site SITE trigger-scheduler-event your.module.functionruns it now. - Is a worker listening to its queue?
bench --site SITE show-pending-jobs. - Did it fail or hit its timeout? Scheduled Job Log and Error Log hold the traceback.
Is "Process Not Found" an outage?
Not always. The System Health Report checks the scheduler by trying to take the same file lock the scheduler holds. Where the web and scheduler containers do not share that file, the check cannot see the lock and reports Process Not Found while the scheduler runs. Read Scheduled Job Log freshness instead.
Where this comes from
Read at Frappe v16.52.0 and ERPNext v16.50.0. “(v15)” marks Frappe v15.123.0 and “(ERPNext v15)” marks ERPNext v15.122.0. Each link opens that file at that tag, on that line.
utils/scheduler.py:26 · utils/scheduler.py:38–60 · utils/scheduler.py:78–91 · utils/scheduler.py:262–264 · utils/scheduler.py:150–177 · utils/scheduler.py:132–147 · scheduled_job_type/scheduled_job_type.py:71–93 · scheduled_job_type/scheduled_job_type.py:32–47 · scheduled_job_type/scheduled_job_type.py:113–127 · scheduled_job_type/scheduled_job_type.py:187–188 · utils/background_jobs.py:54–73 · scheduled_job_type/scheduled_job_type.py:55–58 · scheduled_job_type/scheduled_job_type.py:143–185 · utils/background_jobs.py:44 · utils/background_jobs.py:717–734 · utils/background_jobs.py:84 · utils/background_jobs.py:204–206 · utils/background_jobs.py:275–290 · scheduled_job_type/scheduled_job_type.py:71–86 · commands/scheduler.py:11–34 · commands/scheduler.py:75–108 · commands/scheduler.py:130–202 · utils/scheduler.py:59–75 · system_health_report/system_health_report.py:191–200 · scheduled_job_type/scheduled_job_type.py:29–44 (v15) · scheduled_job_type/scheduled_job_type.py:182–183 (v15) · utils/background_jobs.py:50–52 (v15) · erpnext/hooks.py:466–534 (ERPNext) · utils/scheduler.py:192–231
Official page: Background Jobs, read 14 October 2026. Case: the scheduler that shows Process Not Found in Docker.