NightFox

NightFox

PHP CLI Scripts, Cron Jobs, and Automation

PHP isn't just for web pages. A lot of the PHP I write never touches a browser: imports, queue workers, nightly cleanup, syncing with some other system's API, the little jobs that keep everything running. If you already know PHP, you don't need to switch languages for these.

A safe CLI entry point

#!/usr/bin/env php
<?php
declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "This command must run from the CLI.\n");
    exit(1);
}

$options = getopt('', ['file:', 'dry-run']);
$filename = $options['file'] ?? null;
$dryRun = array_key_exists('dry-run', $options);

if (!is_string($filename) || !is_readable($filename)) {
    fwrite(STDERR, "Usage: import.php --file=/path/data.csv [--dry-run]\n");
    exit(2);
}

fwrite(STDOUT, $dryRun ? "Dry-run mode enabled.\n" : "Import starting.\n");

Exit codes matter

CodeMeaning
0Successful completion
1General runtime failure
2Usage or validation error

Cron, systemd, and monitoring all look at the exit code to decide whether a job worked. Exit 0 when it worked and something else when it didn't, or failures will look like successes.

Prevent overlapping runs

<?php
declare(strict_types=1);

$lock = fopen(sys_get_temp_dir() . '/customer-sync.lock', 'c');

if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
    fwrite(STDERR, "Another sync is already running.\n");
    exit(1);
}

try {
    // Run the synchronization.
} finally {
    flock($lock, LOCK_UN);
    fclose($lock);
}

Design for retries

  • Make operations idempotent whenever possible.
  • Checkpoint progress for large imports.
  • Use unique external IDs to prevent duplicates.
  • Set timeouts on network calls.
  • Retry transient failures with backoff and a limit.
  • Send permanent failures to a review queue.

Cron example

*/5 * * * * /usr/bin/php /var/www/app/bin/process-queue.php \
  >> /var/log/app/process-queue.log 2>&1

Use absolute paths. Cron runs with a tiny PATH and different environment variables than your shell, which is why "it works when I run it by hand" is such a classic.

Long-running workers

Workers that run for hours or days need extra care. Don't hold transactions open, reconnect when the database drops you, keep an eye on memory, and restart the worker after a set number of jobs. Also make sure deployments restart workers. Otherwise they'll happily keep running last week's code.

Last Modified: October 12, 2026, 11:46 am