Back to Blog
·Cron Crew Team

PHP Cron Job Monitoring: A Complete Guide

PHP remains widely used for web development and scheduled tasks are core to most apps. This guide covers practical monitoring approaches with code examples.

PHP Cron Job Monitoring: A Complete Guide

PHP Cron Job Monitoring: A Complete Guide

PHP remains one of the most widely used languages for web development, and scheduled tasks are a core part of most PHP applications. Whether you are running vanilla PHP scripts, Symfony console commands, or custom scheduling implementations, knowing that your jobs actually run is critical. This guide covers practical approaches to monitoring PHP cron jobs with code examples you can adapt to your projects. For foundational concepts, see our complete guide to cron job monitoring.

PHP Scheduling Patterns

PHP applications typically handle scheduled tasks in one of several ways:

System cron with PHP scripts: The most common pattern. You write a PHP script and schedule it with system cron:

0 * * * * /usr/bin/php /var/www/app/scripts/hourly-task.php

Symfony Console commands: Symfony applications use the Console component to create commands that can be scheduled:

0 6 * * * /usr/bin/php /var/www/app/bin/console app:daily-report

Custom scheduling implementations: Some applications implement their own task scheduling, running a single entry point that dispatches to different jobs based on configuration.

Regardless of the pattern, the monitoring approach remains similar: signal when the job starts, signal when it completes (or fails), and let an external service track execution.

Basic PHP Script Monitoring

The simplest approach uses file_get_contents to ping your monitoring endpoint. This works in any PHP environment without additional dependencies.

<?php

$monitorUrl = getenv('MONITOR_URL') ?: 'https://ping.example.com/daily-task';

// Signal start
file_get_contents("{$monitorUrl}/start");

try {
    // Your job logic
    processData();
    cleanupOldRecords();

    // Signal success
    file_get_contents($monitorUrl);
} catch (Exception $e) {
    // Signal failure
    file_get_contents("{$monitorUrl}/fail");

    // Log the error
    error_log("Job failed: " . $e->getMessage());

    // Re-throw to ensure non-zero exit code
    throw $e;
}

This pattern provides three pieces of information to your monitoring service:

  1. The job started (via /start endpoint)
  2. The job completed successfully (via base URL)
  3. The job failed (via /fail endpoint)

Your monitoring service uses this information to detect missed runs, failures, and measure job duration.

Using cURL for Reliability

The file_get_contents function works but has limitations. It lacks timeout control and can hang if the monitoring service is slow to respond. The cURL extension provides better control:

<?php

function pingMonitor(string $url): void
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 3,
    ]);

    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if (curl_errno($ch) || $httpCode >= 400) {
        error_log("Monitor ping failed: " . curl_error($ch));
    }

    curl_close($ch);
}

$monitorUrl = getenv('MONITOR_URL');

pingMonitor("{$monitorUrl}/start");

try {
    runMyJob();
    pingMonitor($monitorUrl);
} catch (Exception $e) {
    pingMonitor("{$monitorUrl}/fail");
    throw $e;
}

The timeout settings ensure that monitoring issues do not cause your job to hang. If the monitoring service is unavailable, the ping fails quickly and your job continues.

Guzzle HTTP Client Approach

For applications already using Guzzle, leverage the HTTP client you have:

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

$client = new Client([
    'timeout' => 10,
    'connect_timeout' => 5,
]);

$monitorUrl = getenv('MONITOR_URL');

try {
    $client->get("{$monitorUrl}/start");
} catch (GuzzleException $e) {
    error_log("Monitor start ping failed: " . $e->getMessage());
}

try {
    processData();

    try {
        $client->get($monitorUrl);
    } catch (GuzzleException $e) {
        error_log("Monitor success ping failed: " . $e->getMessage());
    }
} catch (Exception $e) {
    try {
        $client->get("{$monitorUrl}/fail");
    } catch (GuzzleException $e) {
        error_log("Monitor fail ping failed: " . $e->getMessage());
    }
    throw $e;
}

Notice the nested try-catch blocks for monitoring calls. This ensures that monitoring failures do not interfere with your job execution or error reporting.

Symfony Console Command Monitoring

Symfony Console commands benefit from structured error handling and dependency injection. Here is a monitored command implementation:

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface;

#[AsCommand(name: 'app:daily-report')]
class DailyReportCommand extends Command
{
    public function __construct(
        private HttpClientInterface $httpClient,
        private LoggerInterface $logger,
        private string $monitorUrl,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->pingMonitor("{$this->monitorUrl}/start");

        try {
            $this->generateReport($output);
            $this->pingMonitor($this->monitorUrl);
            return Command::SUCCESS;
        } catch (\Exception $e) {
            $this->pingMonitor("{$this->monitorUrl}/fail");
            $this->logger->error('Daily report failed', ['exception' => $e]);
            throw $e;
        }
    }

    private function pingMonitor(string $url): void
    {
        try {
            $this->httpClient->request('GET', $url, ['timeout' => 10]);
        } catch (\Exception $e) {
            $this->logger->warning('Monitor ping failed', [
                'url' => $url,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function generateReport(OutputInterface $output): void
    {
        // Report generation logic
        $output->writeln('Generating report...');
    }
}

Configure the monitor URL in your services.yaml:

services:
    App\Command\DailyReportCommand:
        arguments:
            $monitorUrl: '%env(DAILY_REPORT_MONITOR_URL)%'

Reusable Monitoring Trait

If you have multiple scripts or commands that need monitoring, create a reusable trait:

<?php

trait Monitorable
{
    protected function withMonitoring(string $url, callable $job): mixed
    {
        $this->pingMonitor("{$url}/start");

        try {
            $result = $job();
            $this->pingMonitor($url);
            return $result;
        } catch (\Exception $e) {
            $this->pingMonitor("{$url}/fail");
            throw $e;
        }
    }

    protected function pingMonitor(string $url): void
    {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_CONNECTTIMEOUT => 5,
        ]);
        curl_exec($ch);

        if (curl_errno($ch)) {
            error_log("Monitor ping failed for {$url}: " . curl_error($ch));
        }

        curl_close($ch);
    }
}

Use the trait in your job classes:

<?php

class DataImportJob
{
    use Monitorable;

    public function run(): void
    {
        $this->withMonitoring(getenv('IMPORT_MONITOR_URL'), function () {
            $this->importData();
            $this->validateResults();
        });
    }

    private function importData(): void
    {
        // Import logic
    }

    private function validateResults(): void
    {
        // Validation logic
    }
}

Best Practices

Handle network failures gracefully: Monitoring should enhance your jobs, not break them. Always catch exceptions from monitoring calls and log them locally.

Set appropriate timeouts: Use short timeouts (5-10 seconds) for monitoring HTTP calls. A slow monitoring service should not delay your job completion.

Use environment variables for URLs: Store monitoring URLs in environment variables or configuration files. This keeps sensitive URLs out of code and allows different URLs per environment.

$monitorUrl = getenv('CRON_MONITOR_URL');
if (!$monitorUrl) {
    error_log('CRON_MONITOR_URL not set, monitoring disabled');
    // Continue without monitoring
}

Log monitoring failures locally: If a monitoring ping fails, write to your application logs. This creates a backup record even if external monitoring is unavailable.

Consider async pings for non-critical monitoring: If you do not need to wait for the monitoring response, use non-blocking requests. However, ensure the failure ping has time to complete before your script exits.

Common PHP Cron Job Patterns

Different types of jobs benefit from monitoring in different ways:

Email queue processing: Monitor each queue processor run. Track duration to detect queue backup (longer runs mean more emails waiting).

Report generation: Monitor for completion and track duration. Growing duration often indicates data growth or query performance issues.

Data imports and exports: These jobs often interact with external systems and are prone to failures. Monitor every run.

Cache warming: Monitor to ensure your cache stays fresh. A failed cache warm job can cause performance issues across your application.

Database cleanup: Maintenance jobs that remove old records or optimize tables should be monitored to ensure they complete within maintenance windows.

Conclusion

Monitoring PHP cron jobs does not require complex infrastructure or expensive tools. With a few lines of code, you can gain visibility into whether your scheduled tasks run successfully.

Start with your most critical jobs: anything that impacts revenue, customer communication, or data integrity. Add the monitoring pattern, configure your endpoints, and set up alerts. As you build confidence in the approach, expand coverage to all scheduled tasks.

If you work with PHP frameworks, see our guides on Laravel task scheduling monitoring for Laravel's elegant scheduler integration or WordPress cron monitoring for WooCommerce stores. For help choosing a monitoring service, check out our cron monitoring pricing comparison.

Cron Crew makes PHP cron monitoring straightforward with simple HTTP endpoints and flexible alerting. Create monitors for your jobs and start receiving alerts when things go wrong. The peace of mind from knowing your scheduled tasks are running is worth the few minutes of setup time.