Troubleshooting · Laravel

Laravel scheduler not running? Fix the silent failure

Why Laravel's scheduler silently stops — the missing cron entry, wrong user, env, and overlapping tasks — and how to fix it.

Laravel’s scheduler is driven by a single system cron entry. If that entry is wrong — or the environment under it is — every scheduled task silently stops. Here’s the checklist.

1. The one cron entry must exist

Laravel needs exactly one crontab line calling schedule:run every minute. Confirm it’s there for the correct user (usually your deploy user, not root):

crontab -l

It should read:

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

If it’s missing, add it with crontab -e. A common deploy mistake is adding it as root while the app runs as www-data (or vice versa).

2. Use the right PHP binary and absolute path

Under cron, php may not resolve, or may be the wrong version. Use the full path and the project’s path:

* * * * * cd /var/www/app && /usr/bin/php8.3 artisan schedule:run >> /dev/null 2>&1

3. The environment differs from your shell

Cron doesn’t load your shell profile, so anything you set there is missing. Laravel reads .env, which is fine — but if your tasks shell out to other binaries, give them an explicit PATH. Also make sure APP_ENV and queue/cache config match production.

4. You silenced the output and now can’t debug

>> /dev/null 2>&1 hides everything, including errors. Temporarily log it:

* * * * * cd /var/www/app && php artisan schedule:run >> storage/logs/schedule.log 2>&1

Then run php artisan schedule:run by hand and read the output — most failures (permissions, missing env, DB connection) show up immediately.

5. Tasks overlap or hang

A long task that runs every minute can pile up. Use Laravel’s guards so a slow run doesn’t block the next:

$schedule->command('reports:build')
    ->hourly()
    ->withoutOverlapping()
    ->onOneServer();

The deeper problem: the scheduler can stop and stay quiet

If schedule:run stops firing — the server rebooted, the crontab was wiped on deploy, PHP was upgraded — Laravel has no way to tell you. Your queued reports and emails just stop.

Ping a heartbeat from a scheduled task so you know the scheduler itself is alive:

$schedule->call(function () {
    Http::timeout(10)->get('https://ping.steadycron.com/<your-ping-token>');
})->everyFifteenMinutes();

If that ping goes missing, SteadyCron alerts you — the scheduler is down, before your users notice.