Skip to content
LastPing
GUIDE · PHP · FREE

Monitor PHP cron jobs: Laravel scheduler callbacks or plain try/catch

In Laravel, attach onSuccess and onFailure callbacks to any scheduled command: one HTTP call each. Without a framework, a try/catch plus file_get_contents is all you need: ping on success, POST the exception to /fail on failure. Absence detection covers PHP fatal errors and OOM kills that bypass every catch block. Free, no monitor cap.

Your assistant can do this for you

Monitor my PHP scripts with LastPing

On this page
  1. The snippets
  2. What this catches
  3. Questions people ask

The snippets

Create a monitor in the console, set the schedule plus a grace window, copy the ping URL, and add it to your script or scheduler config.

Plain PHP: file_get_contents, zero dependencies

file_get_contents with a stream context sends both the GET success ping and the POST failure body. Re-throwing keeps the exit code non-zero so your cron entry still sees the failure:

<?php
declare(strict_types=1);

define('PING', getenv('LASTPING_URL') ?: 'https://ping.lastping.dev/<your-monitor-id>');

function runJob(): void {
    // your job
}

try {
    runJob();
    file_get_contents(PING); // success ping
} catch (\Throwable $e) {
    $ctx = stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => "Content-Type: text/plain\r\n",
            'content' => substr($e->getMessage(), 0, 10_000),
        ],
    ]);
    @file_get_contents(PING . '/fail', false, $ctx);
    throw $e; // re-throw so cron sees exit code != 0
}

Laravel scheduler: onSuccess / onFailure

In app/Console/Kernel.php, chain onSuccess and onFailure onto any scheduled command. The onFailure closure receives a Throwable argument in Laravel 10+:

// app/Console/Kernel.php
use Illuminate\Support\Facades\Http;

protected function schedule(Schedule $schedule): void
{
    $ping = config('services.lastping.url',
                    'https://ping.lastping.dev/<your-monitor-id>');

    $schedule->command('app:nightly-etl')
        ->daily()
        ->onSuccess(function () use ($ping) {
            Http::get($ping);
        })
        ->onFailure(function (\Throwable $e) use ($ping) {
            Http::post($ping . '/fail', [
                'body' => substr($e->getMessage(), 0, 10_000),
            ]);
        });
}

Laravel artisan command: inside the handle method

For commands that are invoked outside the scheduler (e.g. from Jenkins or a deployment hook), add the try/catch directly inside the handle() method. Return a non-zero exit code on failure so the caller can detect it:

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class NightlyEtl extends Command
{
    protected $signature = 'app:nightly-etl';

    public function handle(): int
    {
        $ping = config('services.lastping.url');

        try {
            // your job
            Http::get($ping);
            return self::SUCCESS;
        } catch (\Throwable $e) {
            $this->error($e->getMessage());
            Http::post($ping . '/fail', ['body' => substr($e->getMessage(), 0, 10_000)]);
            return self::FAILURE;
        }
    }
}

register_shutdown_function for fatal errors

PHP fatal errors and parse errors terminate the process before any catch block runs. A shutdown function can catch the last error, but absence-detection is still the safer net: if PHP can't even bootstrap, no shutdown function fires. The missing ping opens the incident regardless. See also the LastPing MCP server for AI-agent provisioning and the Node.js guide for a comparison pattern.

register_shutdown_function(function () {
    $err = error_get_last();
    if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
        $ping = getenv('LASTPING_URL');
        $ctx  = stream_context_create([
            'http' => [
                'method'  => 'POST',
                'header'  => "Content-Type: text/plain\r\n",
                'content' => substr("{$err['message']} in {$err['file']}:{$err['line']}", 0, 10_000),
            ],
        ]);
        @file_get_contents($ping . '/fail', false, $ctx);
    }
});

What this catches

The catch reports what it can; the silence reports the rest.

  • A thrown exception or error: the /fail ping opens an incident immediately with the exception message stored on the ping event.
  • A PHP fatal error: the shutdown function catches the last error and POSTs it to /fail before the process exits.
  • OOM kill or SIGKILL: no ping fires; the missing ping opens the incident after the grace window.
  • The script never ran: missing cron entry, dead server, broken include path. Absence → incident.
  • Recovery: the next successful run closes the incident and sends a recovery notice automatically.

Questions people ask

Front-loaded answers: the most important fact first.

  • How do I monitor a PHP cron job?

    Wrap your script in try/catch: on success call file_get_contents('https://ping.lastping.dev/<your-monitor-id>'); on failure POST the exception message to the /fail endpoint and re-throw. If the script never runs or is killed, the missing ping opens an incident after the grace window.

  • How do I add this to a Laravel scheduled command?

    Use the onSuccess() and onFailure() fluent methods on the scheduled command in app/Console/Kernel.php. The onSuccess callback fires after a zero-exit run; onFailure fires on a non-zero exit or thrown exception. Use Laravel's Http facade to send the ping.

  • How does Laravel's onFailure know the exception?

    In Laravel 10+ the onFailure closure signature accepts a \Throwable argument. Laravel injects it automatically if you declare it as a parameter. In earlier versions use withoutOverlapping() combined with a custom command that returns a non-zero exit code.

  • Do I need Composer or a package?

    No. file_get_contents() with a stream context sends both the GET success ping and the POST failure ping without any dependencies. Guzzle or Laravel's Http facade work too, but they are optional.

  • What catches a PHP fatal error, parse error or OOM kill?

    Fatal errors and OOM kills terminate the process before any catch block runs. Register a shutdown function with register_shutdown_function and check error_get_last(). For the most severe cases (OOM kills, SIGKILL), absence-detection monitoring is the real safety net: no ping reaches the server, and the missing ping itself opens an incident after the grace window.

  • Is PHP job monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

Fatal errors don't reach your catch block: absence does.

One callback now beats a quiet ETL gap later. First alert wired up in about a minute, and LastPing monitors itself the same way.