PHP cookbook · Beginner

Run scheduled jobs (cron) reliably in PHP

Run nightly reports and cleanups without overlapping runs or silent failures.

Approach

Use cron (or a systemd timer) to invoke a PHP CLI script, guard it with a lock so runs never overlap, log start/finish/duration, exit non-zero on failure, and send a heartbeat to a monitoring service (dead man's switch) so missed runs alert you.

Solution

<?php
// bin/nightly-report.php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';

$lock = fopen('/tmp/nightly-report.lock', 'c');
if (!flock($lock, LOCK_EX | LOCK_NB)) {
    fwrite(STDERR, "Previous run still in progress\n");
    exit(0);
}

$start = microtime(true);
try {
    $count = generateNightlyReport(createPdo());
    $logger->info('nightly report done', ['rows' => $count, 'seconds' => round(microtime(true) - $start, 1)]);
    file_get_contents(getenv('HEARTBEAT_URL'));          // e.g. healthchecks.io ping
    exit(0);
} catch (Throwable $e) {
    $logger->error('nightly report failed', ['exception' => $e]);
    exit(1);
} finally {
    flock($lock, LOCK_UN);
}
# crontab -e  (UTC)
15 2 * * * /usr/bin/php /var/www/app/bin/nightly-report.php >> /var/log/app/cron.log 2>&1

Pitfalls

  • Overlapping runs when a job takes longer than its interval.
  • Cron running a different PHP version/ini than the web server.
  • On multiple servers, every server runs the job (use a DB/Redis lock or run on one host).

Variations

  • Laravel scheduler: $schedule->command('report:nightly')->dailyAt('02:15')->withoutOverlapping()->onOneServer().
  • Symfony Scheduler component with Messenger.

Further reading: PHP manual: flock

Reference: https://www.php.net/manual/en/function.flock.php

More PHP recipes