Frappe Scheduled Jobs Not Running: The Full Checklist Nobody Gives You
Your scheduler_events hook is defined. The scheduler is "enabled". And yet your daily report never sends, your auto-email digest is silent, and that cleanup job you wrote three weeks ago has never run once. You search the Frappe forum and find the same thread posted six times across six years, and every reply is "run bench enable-scheduler" or "try bench migrate", with no explanation of why that would help or what to check when it does not.
The reason those answers keep failing is that "the scheduler" is not one switch. It is a chain of four independent things that must all be true at once: a cron entry that ticks the scheduler, a per-site enabled flag, running background workers, and a queue that is not jammed by a stuck job. Break any link and every scheduled job goes silent, with no error anywhere obvious. The forum answers each fix one link and assume it was the broken one.
This is the whole chain, checked in order, so you find the actual broken link instead of guessing.
Frappe scheduled jobs need all four of these, and they fail silently if any is missing:
- The scheduler tick: a long-running
bench scheduleprocess must be running to enqueue due jobs every minute. In development it is part ofbench start; in production it is a supervisor program. No tick, nothing runs. - The per-site flag:
bench --site SITE scheduler statusmust say enabled. Enable withbench --site SITE scheduler enable. - Running workers:
bench doctormust show workers online. In production that means supervisor is running the worker processes. - An unblocked queue: a stuck RQ job can freeze the whole queue. Find it with
bench doctor/show-pending-jobsand clear it.
The symptom: enabled but nothing fires
There is no error message, which is exactly why this is maddening. The job simply never runs. You will see nothing in the UI, nothing in your app's error log, and the scheduler will cheerfully report itself as "enabled" while doing nothing. The only evidence lives in bench doctor and the log files, and you have to go looking.
Why "just enable the scheduler" so often does nothing
Enabling the scheduler sets a per-site flag that says "this site wants its scheduled jobs to run". But that flag is only consulted when something ticks the scheduler, and the thing that ticks it is the long-running bench schedule process, which wakes up every minute and enqueues the due jobs into Redis. Separate worker processes then pull jobs off the queue and execute them.
So there are three moving parts even before you get to a stuck queue: the scheduler process that enqueues, the flag that says "yes, enqueue mine", and the workers that dequeue and run. bench enable-scheduler only touches the middle one. If the scheduler process is not running, or the workers are not running, flipping the flag changes nothing, which is why the advice "just enable it" fails for so many people and they never learn why.
The full checklist, in order
Step 1: Read bench doctor first
From your bench directory, this one command tells you the state of three of the four links:
bench doctor
Read it carefully. It reports, per site, whether the scheduler is inactive or active, how many workers are online, and how many jobs are pending. Interpret it like this:
| What you see | Which link is broken |
|---|---|
Scheduler inactive for SITE | Link 2, the per-site flag |
Workers online: 0 | Link 3, no workers running |
| Many pending jobs, none draining | Link 4, a stuck job or no workers |
| Everything looks fine but jobs still do not run | Link 1, the scheduler process is not running |
Step 2: Confirm the scheduler process is running
This is the link bench doctor cannot fully see, and the one most often missing on a hand-built server or after a restore. In production the scheduler is a supervisor program named something like frappe-bench-frappe-schedule. Confirm it is up:
sudo supervisorctl status | grep -i schedule
It must read RUNNING. If it is missing entirely, the supervisor config was never generated or is stale. Regenerate it and reload supervisor:
# production: supervisor runs the scheduler and workers
bench setup supervisor
sudo supervisorctl reread && sudo supervisorctl update
In development there is no supervisor. The scheduler runs as the schedule line of the Procfile that bench start launches, so it only runs while bench start is running. Some older or manual installs instead relied on a crontab entry; if you are on such a setup, check crontab -l | grep -i bench for a line invoking the scheduler. Modern bench manages it as a process, not a cron job.
Step 3: Enable the scheduler for the specific site
The flag is per site, so a multi-site bench can have it on for one site and off for another. Check and set it explicitly:
bench --site your-site.local scheduler status
# Scheduler is disabled -> turn it on:
bench --site your-site.local scheduler enable
Note the distinction people trip over: bench --site SITE scheduler enable sets the site-level flag, which is the same thing the System Settings > Enable Scheduler toggle in the UI controls. There is also a bench-wide pause flag, pause_scheduler in common_site_config.json, which if set to 1 silences every site regardless of the per-site flag. Check that too:
grep pause_scheduler sites/common_site_config.json
Step 4: Confirm workers are actually running
The tick enqueues, but workers execute. If no worker process is alive, jobs pile up in Redis and never run. In development you start them explicitly:
bench worker --queue default,short,long # foreground, for testing
# or the all-in-one dev process that includes workers + schedule:
bench start
In production the workers are supervisor programs. Confirm they are up:
sudo supervisorctl status | grep worker
Every worker line should read RUNNING. A worker in FATAL or STOPPED is a silent scheduler outage.
Step 5: Read the logs the forum never mentions
Two log files tell you whether jobs are being enqueued and whether they are failing inside the worker:
tail -n 50 logs/scheduler.log # did the tick enqueue anything?
tail -n 50 logs/worker.log # did a worker pick it up and error?
scheduler.log proves link 1 is firing. worker.log shows exceptions thrown by your job code, which is a different failure from "the scheduler is off", your job is running and crashing.
Step 6: Find and clear a stuck job blocking the queue
A single job that hangs, an infinite loop, a request to a dead upstream with no timeout, can occupy a worker forever, and if all workers are stuck the whole queue freezes. List pending work:
bench --site your-site.local show-pending-jobs
If jobs are piling up and never draining, and workers are "running" but doing nothing, you have a stuck job. The blunt fix is to purge the queue and restart the workers so healthy jobs can flow again:
bench purge-jobs # clears queued/failed jobs
sudo supervisorctl restart all # or: restart the worker programs
Then fix the job that hung, usually by adding a timeout, so it does not jam the queue again. This is one reason I argue in cron vs systemd timers vs the Frappe scheduler that you should choose your scheduling mechanism by its failure mode, and a shared queue that one bad job can freeze is a very specific failure mode.
Verification: prove a job actually runs
Do not wait a day to see if the daily job fired. Enqueue a trivial job right now and watch it complete:
bench --site your-site.local execute frappe.ping
Then trigger the scheduler manually to force the whole enqueue path once:
bench --site your-site.local trigger-scheduler-event all
Watch logs/worker.log fill as jobs run. If frappe.ping runs but your event does not, the problem is not the scheduler at all, it is your scheduler_events hook definition or the job raising an exception, and worker.log will show it.
Docker: the scheduler needs its own process
If you run Frappe with the official Docker images, this trips almost everyone once. The backend container does not run the scheduler. The scheduler and the workers are separate services in the compose file (typically scheduler, queue-short, queue-long). If you only started the web container, nothing schedules and nothing works the queue. Confirm all of them are up:
docker compose ps | grep -E 'scheduler|queue'
Each must be running. There is no cron inside these containers, the dedicated scheduler service replaces it. Scaling the web service alone will never make scheduled jobs run.
What people get wrong
- "Just run
bench enable-scheduler." That sets one of four links. If the scheduler process is not running or workers are down, it changes nothing, and this is why the same forum thread exists six times. - "Run
bench migrate." Migration has nothing to do with the scheduler running. It occasionally "works" only because it restarts processes as a side effect, which masks the real fix, restarting the workers. - Assuming enabled in one site means enabled everywhere. The flag is per site. Check the specific site.
- Ignoring
worker.log. If your job runs and crashes, the scheduler is working perfectly. That is a code bug, not a scheduler bug, and they need opposite fixes. - On Docker, scaling the backend and expecting jobs to run. The scheduler is a separate container. It is not optional.
When it is still broken
- Everything checks out but jobs still do not fire. Confirm Redis is reachable, workers connect to Redis to receive jobs. A Redis outage looks exactly like a dead scheduler. This is the same layer that produces the No module named redis.commands startup error.
- Only long-running jobs never finish. They are hitting the queue timeout or hanging. Add a timeout to the job and check the
longqueue worker specifically. - Jobs run but hours late. Your workers are saturated. Add worker processes or move heavy jobs to the
longqueue. - It works after restart then stops again. A worker is dying under a specific job. Watch
worker.logfor the crash and fix that job.
The scheduler is not a switch, it is a chain. Check the four links in order, stop at the first false one, and you will fix it in ten minutes instead of posting the seventh copy of that forum thread.
Frequently asked questions
- Why is my Frappe scheduler enabled but jobs still not running?
- Because 'enabled' is only one of four requirements. Scheduled jobs also need the long-running bench schedule process to enqueue due jobs every minute, running worker processes to execute them, and a queue that is not frozen by a stuck job. Enabling the scheduler flag does nothing if the scheduler process is not running or the workers are down. Run bench doctor and check supervisor to see which link is broken.
- What does bench doctor tell me about the scheduler?
- bench doctor reports, per site, whether the scheduler is active or inactive, how many workers are online, and how many jobs are pending. 'Scheduler inactive' means the per-site flag is off; 'Workers online: 0' means no worker is running to execute jobs; many pending jobs that never drain means the queue is stuck or workerless. It cannot confirm the scheduler tick process itself, so also check supervisorctl status.
- My Frappe scheduler works in development but not in Docker. Why?
- The official Frappe Docker images run the scheduler and workers as separate services, not inside the backend container. There is no cron inside the containers; a dedicated scheduler service replaces it. If you only started the web or backend container, nothing enqueues or works the queue. Confirm the scheduler and queue services are running with docker compose ps.
- How do I fix a stuck job blocking the whole Frappe queue?
- A single hung job can occupy a worker forever, and if all workers are stuck the queue freezes and no scheduled job runs. Use bench --site SITE show-pending-jobs to see the backlog, then bench purge-jobs to clear queued and failed jobs and restart the workers so healthy jobs flow again. Fix the offending job, usually by adding a timeout, so it does not jam the queue again.