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 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.phpSymfony 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-reportCustom 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:
- The job started (via
/startendpoint) - The job completed successfully (via base URL)
- The job failed (via
/failendpoint)
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.