Skip to content

Monitor the Laravel scheduler

Laravel’s scheduler runs everything from one cron entry, so a single problem can stop every task at once: the schedule:run cron line goes missing on a new server, the queue worker dies, or one task starts throwing. A heartbeat check per important task catches this: the task reports to Pingstack after it succeeds, and Pingstack alerts you when that report doesn’t arrive on time.

Laravel already knows how to send these reports. Its pingOnSuccess hook calls a URL after a task finishes successfully, so this recipe needs no extra packages.

1. Create a heartbeat check that matches the task

Section titled “1. Create a heartbeat check that matches the task”

Find the task in routes/console.php (Laravel 11 and later) or app/Console/Kernel.php (earlier versions):

Schedule::command('reports:send')->dailyAt('02:00');

Create a heartbeat check and set its schedule to Cron with the matching expression (0 2 * * * for dailyAt('02:00')). Run php artisan schedule:list to see each task’s cron expression and next run time.

Laravel runs tasks in your application’s timezone (APP_TIMEZONE, or timezone in config/app.php) unless the task sets ->timezone(...). Choose the same timezone in Pingstack. See schedules and timezones.

Set a grace period longer than the task’s usual runtime, plus a margin. Attach a notification group.

Copy the location’s ping URL from the check in Pingstack and add it to the server’s .env, not to code:

Terminal window
PINGSTACK_REPORTS_PING_URL=

Read it through a config file, because env() returns null outside config files once you run php artisan config:cache. In config/services.php:

'pingstack' => [
'reports_ping_url' => env('PINGSTACK_REPORTS_PING_URL'),
],

Add pingOnSuccess to the task:

use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:send')
->dailyAt('02:00')
->withoutOverlapping()
->onOneServer()
->pingOnSuccess(config('services.pingstack.reports_ping_url'));

Laravel sends the request only when the command exits successfully, so a failed run sends nothing and Pingstack alerts you when the deadline passes. withoutOverlapping() stops a slow run from overlapping the next, and onOneServer() keeps several app servers from each running and reporting the task (it needs a shared cache such as Redis or the database).

On Laravel 10 and earlier, use the same chain on $schedule->command(...) inside schedule() in app/Console/Kernel.php.

Don’t use thenPing for this. It pings after every run, including failed ones, which would report a failing task as healthy.

Run the task once through the scheduler’s test command:

Terminal window
php artisan schedule:test --name="reports:send"

Open the check in Pingstack and confirm the location received a heartbeat and shows a next deadline. If it didn’t, check that config('services.pingstack.reports_ping_url') isn’t empty in php artisan tinker.

Then confirm the scheduler itself runs on the server. The cron entry should look like this:

Terminal window
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

If you use php artisan schedule:work under a process manager instead, check that the process is running.

pingOnSuccess only sends the URL. To send a count as a numeric value, report from the end of the command’s handle method instead, after the work succeeds:

use Illuminate\Support\Facades\Http;
Http::timeout(10)->retry(3, 1000)->get(
config('services.pingstack.reports_ping_url'),
['value' => $sentCount],
);

Then remove pingOnSuccess from that task so it doesn’t report twice, and set warning and critical thresholds on the check.

Give each task you care about its own check and its own variable. If the whole scheduler stops, every check alerts, which tells you the cause is the scheduler rather than one task.

  • Timezone mismatch. The check and the app’s or task’s timezone must agree.
  • Config cache. After adding the variable, run php artisan config:cache again, or the URL is empty and no report is sent.
  • Background tasks. With ->runInBackground(), Laravel learns the result when the process finishes, so the report arrives after the work completes, not when the scheduler moves on.
  • Maintenance mode. Tasks don’t run in maintenance mode unless they call ->evenInMaintenanceMode().

If alerts still don’t match what you expect, see troubleshooting monitoring.