Laravel scheduler not running: find the cause, then let Laravel's ping hooks report every run
Laravel runs every scheduled task from one cron entry that calls schedule:run each minute. When that entry is missing, or a task is skipped, nothing fails and nothing is logged where you would look. Work through the causes below, then chain pingBefore, pingOnSuccess and pingOnFailure onto the task: built-in hooks, no package, calling LastPing's start, success and fail URLs.
Your assistant can do this for you
Find out why my Laravel scheduled tasks are not running, and monitor them with LastPing
On this page
Check these first: three commands
Run them on the server, as the user cron runs as.
# What Laravel has registered, and when each task is next due
php artisan schedule:list
# Run the scheduler once by hand
cd /path-to-your-project && php artisan schedule:run
# Is the cron entry there, for this user?
crontab -l
If schedule:run prints No scheduled commands are ready to run., the scheduler itself works: no task was due in that minute, or each due task was skipped, for example by a when() or environments() constraint, by maintenance mode or by a pause. If a task shows in schedule:list and runs by hand but never on its own, the cron entry is the problem.
Nine reasons Laravel's scheduler does not run a task
In the order worth checking, quoting the Laravel documentation where it is exact.
There is no cron entry, or it is not every minute
Laravel runs nothing by itself. One cron entry must call schedule:run every minute, and that command decides which tasks are due. The entry from Laravel's documentation sends all output to /dev/null, so while you debug, point it at a log file instead.
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Cron cannot find php, or runs as someone else
Cron starts jobs with a minimal environment, so a php that works in your shell may not be on cron's PATH. Use the full path from which php, and install the entry in the crontab of the user that runs the application. The cron job not running guide covers the PATH and environment causes in detail.
The schedule is defined where Laravel does not read it
In current Laravel versions the schedule lives in routes/console.php, or in a withSchedule closure in bootstrap/app.php. An example written for app/Console/Kernel.php, copied into an application that has no such file, registers nothing. php artisan schedule:list shows exactly what is registered and when each task is next due.
The application is in maintenance mode
Laravel's documentation: "Your application's scheduled tasks will not run when the application is in maintenance mode." A deploy that fails between php artisan down and php artisan up therefore skips every task until someone notices. Tasks that must run anyway take evenInMaintenanceMode().
The scheduler is paused
In Laravel 13, php artisan schedule:pause stops all scheduled task processing without a code change, and nothing runs until php artisan schedule:continue. A task marked evenWhenPaused() still runs.
An old overlap lock is still held
withoutOverlapping() takes a lock in your application's cache, and by default the lock expires after 24 hours. If a server dies mid-run, the task is skipped until the lock expires. php artisan schedule:clear-cache clears these locks; the documentation describes it as "typically only necessary if a task becomes stuck due to an unexpected server problem."
A long task holds up the ones after it
"By default, multiple tasks scheduled at the same time will execute sequentially based on the order they are defined." A slow task makes every later one start late. runInBackground() runs a command or exec task in the background so they can run at the same time.
A timezone with daylight saving
timezone() on a task, or schedule_timezone in the app configuration, makes the times local. The documentation warns: "When daylight saving time changes occur, your scheduled task may run twice or even not run at all", and recommends avoiding timezone scheduling where possible. The cron expression explainer shows which runs a change skips or repeats.
In a container, nothing runs the scheduler
A container runs only the process it was started with, so a crontab inside it does nothing unless a cron daemon runs there too. Laravel presents php artisan schedule:work for local development: it runs in the foreground and invokes the scheduler every minute until it is stopped. Whatever starts the scheduler in a container has to keep it running; if that process exits, no task runs and nothing reports it.
Report every run with Laravel's own ping hooks
Laravel can call a URL before a task starts and after it succeeds or fails. Those hooks map straight onto LastPing's start, success and fail URLs, and the monitor's schedule covers what no hook can report: a task that never runs at all.
Create a monitor with the task's schedule
In the console, create a monitor with the task's cron expression (schedule:list prints it), in the timezone the task runs in, plus a grace window. Copy the ping URL into your configuration:
// config/services.php
'lastping' => [
'nightly_etl' => env('LASTPING_NIGHTLY_ETL_URL'),
],
# .env
LASTPING_NIGHTLY_ETL_URL=https://ping.lastping.dev/<your-monitor-id>
Chain the ping hooks onto the task
Three of Laravel's built-in hooks, no package:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
$ping = config('services.lastping.nightly_etl');
Schedule::command('app:nightly-etl')
->dailyAt('03:00')
->pingBefore("$ping/start") // run started
->pingOnSuccess($ping) // exit code 0
->pingOnFailure("$ping/fail"); // non-zero exit code
pingBeforecalls/start: the run began, which arms overrun detection for a task that hangs.pingOnSuccesscalls the monitor URL when the command exits with code 0.pingOnFailurecalls/failwhen it exits non-zero, and the alert goes out at once.
Leave thenPing for the success URL out
thenPing pings after the task whatever its result, so pointed at the success URL it would report a failed run as a success. pingOnSuccess and pingOnFailure follow the exit code. Each ping is a plain GET request, which LastPing accepts, and a ping that cannot be delivered is reported to Laravel's exception handler without stopping the task.
What this catches
The hooks report what the task can see; the schedule reports the silence.
- The scheduler never ran: a missing or broken cron entry, a dead server, a container whose scheduler process exited. No ping arrives in the window, so an incident opens.
- The task was skipped: maintenance mode, a paused scheduler or an overlap lock left behind. Same silence, same incident.
- The command failed: a non-zero exit code calls
/failand the alert goes out immediately. - The command hung: the start ping arrived and the finish never did, so overrun detection opens an incident while it is still running.
- Recovery: the next successful run closes the incident and sends a recovery notice.
Plain PHP scripts without a framework are covered in the PHP guide, which also shows the onSuccess and onFailure callbacks when you want to POST the error text with the failure.
Questions people ask
Front-loaded answers: the most important fact first.
-
Why is my Laravel scheduler not running?
Most often the cron entry that runs php artisan schedule:run every minute is missing, runs as the wrong user or cannot find php. Run php artisan schedule:list to see what is registered, then php artisan schedule:run by hand. Tasks are also skipped in maintenance mode, while the scheduler is paused, and while a withoutOverlapping lock from an earlier run is still held.
-
What does No scheduled commands are ready to run mean?
schedule:run started, but no task was due in that minute, or each due task was skipped, for example by a when() or environments() constraint, by maintenance mode or by a pause. The cron entry worked for that minute.
-
Do Laravel scheduled tasks run in maintenance mode?
No. Laravel skips scheduled tasks while the application is in maintenance mode, unless the task calls evenInMaintenanceMode().
-
How do I monitor Laravel scheduled tasks without a package?
Chain Laravel's ping hooks onto the task: pingBefore to LastPing's /start URL, pingOnSuccess to the monitor URL and pingOnFailure to /fail. Give the monitor the task's cron schedule and a grace window, and a task that never runs opens an incident too.
-
Should I use thenPing?
Not for the success URL. thenPing pings after the task whatever its result, so a failed run would be reported as a success. Use pingOnSuccess and pingOnFailure, which Laravel calls according to the command's exit code.
-
Where is the Laravel schedule defined?
In current Laravel versions, in routes/console.php, or in a withSchedule closure in bootstrap/app.php. php artisan schedule:list shows every registered task and when it is next due.
-
Is this monitoring free?
Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.
The task that stopped running is the one nobody checks.
Three hooks now beat finding a week of missing reports later. First alert wired up in minutes, and LastPing monitors itself the same way.