AlertKite
← AlertKite

Laravel scheduler not running? Here is how to prove it, and how to be told next time

Laravel's scheduler is one cron entry that calls schedule:run every minute. Everything else — queues drained, reports mailed, tokens pruned — hangs off that single line. When it stops, Laravel does not complain. There is no error page and no failed job, because nothing ran to fail.

It is almost always one of four things, and you can tell which in about a minute.

Confirm the scheduler is actually firing, then report in

# 1. Is the cron entry there at all?
crontab -l | grep schedule:run

# 2. Does it run by hand? (as the web user, not root)
sudo -u www-data php /var/www/app/artisan schedule:run

# 3. What does Laravel think is due?
php artisan schedule:list

# 4. Once it works, prove it keeps working:
# * * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1 \
#     && curl -fsS -m 10 https://hb.alertkite.com/p/YOUR_TOKEN

Setting it up

  1. Check the crontab exists and belongs to the right user — root's crontab is the usual wrong answer.
  2. Run schedule:run by hand as the web user; permission errors surface immediately.
  3. Check APP_ENV and any ->environment() constraints, which silently skip tasks in production.
  4. Add a heartbeat ping after schedule:run so a future failure tells you rather than hiding.

The four usual causes

The crontab is on the wrong user, so permissions fail silently. The path is wrong after a deploy moved the release directory. PHP's CLI binary differs from the web one and lacks an extension. Or the task is wrapped in ->environment('local') and was never going to run in production. In every case the symptom is identical: silence.

withoutOverlapping hides a stuck job forever

If a task dies without releasing its lock, withoutOverlapping will skip every subsequent run — permanently, and quietly. The scheduler looks healthy, the cron entry fires every minute, and that one task never runs again. A heartbeat on the task itself, rather than on schedule:run, is what catches this.

Monitor the task, not just the scheduler

schedule:run firing proves the scheduler is alive. It does not prove your nightly report was generated. For anything that matters, ping from inside the task after it succeeds — then a job that throws, or that is skipped by a stale lock, is caught.