Back to Blog
PHP10 min read

PHP Cron Jobs: Schedule Tasks with Crontab, Symfony & More

PHP powers the majority of the web, and most PHP applications need cron jobs for tasks like sending emails, generating reports, cleaning up old data, and syncing with external APIs. This guide covers every approach to scheduling PHP tasks — from raw crontab entries to framework-level schedulers in Symfony and CodeIgniter — along with the pitfalls that trip up most developers.

Running PHP Scripts from Crontab

The most direct way to schedule a PHP task is to add a cron expression to your server's crontab. The entry specifies when to run and the full command to execute your PHP script:

# Run cleanup.php every day at 2:00 AM
0 2 * * * /usr/bin/php /var/www/myapp/cron/cleanup.php

# Run report generator every Monday at 6:00 AM
0 6 * * 1 /usr/bin/php /var/www/myapp/cron/generate_report.php

# Run queue worker every 5 minutes
*/5 * * * * /usr/bin/php /var/www/myapp/cron/process_queue.php

Open your crontab with crontab -e and add lines following this pattern. Use a cron expression generator if you are unsure about the timing syntax.

Always Use Absolute Paths

The number one rule for PHP cron jobs: use absolute paths for everything. Cron runs with a minimal environment — no PATH, no working directory, no shell profile. Find your PHP binary location first:

# Find your PHP binary
which php
# Output: /usr/bin/php

# If you have multiple PHP versions
which php8.2
# Output: /usr/bin/php8.2

# Verify version
/usr/bin/php -v

Setting the Working Directory

If your PHP script uses relative paths (e.g., require '../config.php'), change to the correct directory before running it:

# cd first, then run the script
*/5 * * * * cd /var/www/myapp && /usr/bin/php cron/process_queue.php

# Or pass the working directory as a PHP argument
*/5 * * * * /usr/bin/php -d "include_path=/var/www/myapp" /var/www/myapp/cron/process_queue.php

Capturing Output and Errors

By default, cron emails any output to the crontab owner. Redirect output to a log file for easier debugging:

# Log stdout and stderr to a file
0 2 * * * /usr/bin/php /var/www/myapp/cron/cleanup.php >> /var/log/myapp/cron.log 2>&1

# Suppress all output
0 2 * * * /usr/bin/php /var/www/myapp/cron/cleanup.php > /dev/null 2>&1

# Log errors only
0 2 * * * /usr/bin/php /var/www/myapp/cron/cleanup.php > /dev/null 2>> /var/log/myapp/cron-errors.log

PHP CLI vs Web Execution

A common mistake is writing a PHP cron script as if it will be executed by a web server. Cron jobs run through PHP CLI (Command Line Interface), which differs from the web SAPI in several important ways:

FeaturePHP CLI (Cron)PHP Web (Apache/Nginx)
Configuration filephp-cli.iniphp.ini (apache2/fpm)
max_execution_time0 (unlimited)30 seconds (default)
memory_limit-1 or custom128M (default)
Outputstdout/stderrHTTP response body
$_SERVER variablesMinimal (no HTTP_HOST, REQUEST_URI)Full HTTP context
UserCrontab owner (e.g., www-data)Web server user

Important

PHP CLI uses a different php.ini file than the web SAPI. Extensions enabled in your web config may not be loaded in CLI. Check with php -m to verify which extensions are available for cron jobs.

If your script needs HTTP context variables, define them at the top of your cron script:

<?php
// Set context variables for CLI execution
if (php_sapi_name() === 'cli') {
    $_SERVER['HTTP_HOST'] = 'example.com';
    $_SERVER['REQUEST_URI'] = '/cron/cleanup';
    $_SERVER['DOCUMENT_ROOT'] = '/var/www/myapp/public';
}

Common Pitfalls

Most PHP cron job failures fall into a few predictable categories. Here are the issues that trip up developers most frequently:

1. Wrong PHP Binary or Version

Servers often have multiple PHP versions installed. Your crontab might use a different version than your web server, causing syntax errors or missing extensions:

# Bad: relies on PATH (which cron may not have)
* * * * * php /path/to/script.php

# Good: explicit path to the correct PHP version
* * * * * /usr/bin/php8.2 /path/to/script.php

2. File Permission Errors

The crontab user must be able to read the PHP script and write to any output files or directories. A common scenario: your deployment user owns the files but the crontab runs as www-data.

# Check which user owns the crontab
whoami

# Check file permissions
ls -la /var/www/myapp/cron/cleanup.php

# Fix: ensure the cron user can read the script
chmod 644 /var/www/myapp/cron/cleanup.php

# Fix: ensure the cron user can write to log directory
chown www-data:www-data /var/log/myapp/
chmod 755 /var/log/myapp/

3. php.ini Differences

PHP CLI loads a different php.ini than the web SAPI. This affects loaded extensions, timezone settings, and memory limits:

# Find which php.ini CLI uses
php --ini
# Output: Loaded Configuration File: /etc/php/8.2/cli/php.ini

# Compare with web php.ini
# Web: /etc/php/8.2/fpm/php.ini or /etc/php/8.2/apache2/php.ini

# Override settings inline
*/5 * * * * /usr/bin/php -d memory_limit=512M -d date.timezone=UTC /path/to/script.php

4. Overlapping Executions

If a script takes longer than the cron interval, multiple instances will run simultaneously. Use a lock file to prevent overlaps:

<?php
$lockFile = '/tmp/my_cron_job.lock';

// Check if already running
if (file_exists($lockFile)) {
    $pid = (int) file_get_contents($lockFile);
    // Check if the process is still alive
    if (posix_kill($pid, 0)) {
        echo "Already running (PID: $pid)\n";
        exit(0);
    }
    // Stale lock file — previous run crashed
    unlink($lockFile);
}

// Write our PID
file_put_contents($lockFile, getmypid());

// Register cleanup on exit
register_shutdown_function(function () use ($lockFile) {
    if (file_exists($lockFile)) {
        unlink($lockFile);
    }
});

// --- Your cron job logic here ---
echo "Running...\n";
sleep(30); // Simulate work

Alternatively, use flock at the crontab level for a simpler approach:

# Use flock to prevent overlapping executions
*/5 * * * * flock -n /tmp/my_cron.lock /usr/bin/php /var/www/myapp/cron/process_queue.php

Symfony Scheduler Component

Symfony 6.3+ includes a dedicated Scheduler component that integrates with the Messenger component. Instead of managing individual crontab entries, you define schedules in PHP using attributes and let Symfony handle the timing.

Installation

composer require symfony/scheduler

Creating a Scheduled Task with #[AsCronTask]

The #[AsCronTask] attribute lets you attach a cron expression directly to a message class:

<?php
// src/Scheduler/Message/GenerateReportMessage.php

namespace App\Scheduler\Message;

use Symfony\Component\Scheduler\Attribute\AsCronTask;

#[AsCronTask('0 2 * * *')]  // Every day at 2:00 AM
class GenerateReportMessage
{
    public function __construct(
        public readonly string $reportType = 'daily',
    ) {}
}

Creating a Periodic Task with #[AsPeriodicTask]

<?php
// src/Scheduler/Message/CleanupTempFilesMessage.php

namespace App\Scheduler\Message;

use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;

#[AsPeriodicTask(frequency: '1 hour', from: '02:00', until: '06:00')]
class CleanupTempFilesMessage
{
}

The Message Handler

<?php
// src/Scheduler/Handler/GenerateReportHandler.php

namespace App\Scheduler\Handler;

use App\Scheduler\Message\GenerateReportMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class GenerateReportHandler
{
    public function __invoke(GenerateReportMessage $message): void
    {
        // Your report generation logic
        $this->generateReport($message->reportType);
    }
}

Running the Scheduler

Add a single crontab entry that runs the Symfony messenger consumer:

# Crontab entry for Symfony Scheduler
* * * * * cd /var/www/myapp && php bin/console messenger:consume scheduler_default --time-limit=60 >> /dev/null 2>&1

Tip

In production, use Supervisor or systemd to keep the messenger consumer running continuously instead of relying on crontab. The crontab approach works but adds startup overhead every minute.

CodeIgniter Task Scheduling

CodeIgniter 4 includes a built-in Tasks library for defining scheduled commands. Define your tasks in app/Config/Tasks.php:

<?php
// app/Config/Tasks.php

namespace Config;

use CodeIgniter\Tasks\Scheduler;

class Tasks
{
    public function init(Scheduler $schedule): void
    {
        // Run a command every hour
        $schedule->command('maintenance:cleanup')
            ->hourly()
            ->named('cleanup');

        // Run a closure daily at 3 AM
        $schedule->call(function () {
            $db = db_connect();
            $db->table('sessions')
                ->where('timestamp <', time() - 86400)
                ->delete();
        })->daily('3:00 am')->named('session-cleanup');

        // Run a shell command
        $schedule->shell('curl https://api.example.com/sync')
            ->everyFiveMinutes()
            ->named('api-sync');
    }
}

Then add a single crontab entry to run the CodeIgniter task runner:

# CodeIgniter task runner
* * * * * cd /var/www/myapp && php spark tasks:run >> /dev/null 2>&1

Monitoring PHP Cron Jobs with CronJobPro

Whether you are using raw crontab entries or a framework scheduler, you need monitoring to know when jobs fail silently. CronJobPro provides dead man's switch monitoring that alerts you when a ping is missed.

Wrap Your Crontab Command

The simplest approach — wrap your existing cron command with ping calls:

# Ping on start, run script, ping on success
*/5 * * * * curl -fsS https://cronjobpro.com/ping/abc123/start && /usr/bin/php /var/www/myapp/cron/process_queue.php && curl -fsS https://cronjobpro.com/ping/abc123

Ping from Within PHP

For more control, add monitoring directly inside your PHP script:

<?php
$pingUrl = 'https://cronjobpro.com/ping/abc123';

// Signal start
@file_get_contents($pingUrl . '/start');

try {
    // Your cron job logic
    processQueue();

    // Signal success
    @file_get_contents($pingUrl);
} catch (Exception $e) {
    // Signal failure with error message
    @file_get_contents($pingUrl . '/fail', false, stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: text/plain',
            'content' => $e->getMessage(),
        ],
    ]));
    throw $e; // Re-throw for logging
}

CronJobPro tracks execution duration, success/failure patterns, and sends alerts via email, Slack, or webhook when something goes wrong. Check the cron expression cheatsheet if you need help fine-tuning your schedule.

Frequently Asked Questions

How do I run a PHP script from crontab?

Add a line to your crontab (crontab -e) that specifies the schedule and the full path to both the PHP binary and your script. For example: */5 * * * * /usr/bin/php /var/www/myapp/cron/cleanup.php. Always use absolute paths because cron runs with a minimal environment and does not know your working directory or PATH.

What is the difference between running PHP via CLI and via web server?

PHP CLI uses a separate php.ini configuration file, has no execution time limit by default, outputs to stdout instead of a browser, and does not have access to web server variables like $_SERVER['HTTP_HOST']. Cron jobs always use PHP CLI. If your script was designed for web execution, you may need to adjust it to work correctly from the command line.

Why does my PHP cron job work manually but not from crontab?

The most common cause is environment differences. When you run a script manually, your shell provides PATH, working directory, and environment variables. Cron provides almost none of these. Fix this by using absolute paths for everything, setting the working directory with cd before the script, and ensuring file permissions allow the cron user to read and execute the script.

How do I use Symfony Scheduler for cron jobs?

Symfony 6.3+ includes the Scheduler component. Install it with composer require symfony/scheduler, create a message class, add the #[AsCronTask] or #[AsPeriodicTask] attribute to it, and run the messenger:consume scheduler_default command. The crontab entry runs this Symfony command every minute, and the Scheduler component handles timing internally.

Can I monitor PHP cron jobs without modifying the script?

Yes. With CronJobPro, you can wrap your cron command to ping a monitoring URL on start and completion. For example: curl -fsS https://cronjobpro.com/ping/abc123/start && /usr/bin/php /path/to/script.php && curl -fsS https://cronjobpro.com/ping/abc123. The monitoring service tracks execution, duration, and alerts you if a ping is missed.

Monitor Your PHP Cron Jobs

Get instant alerts when your PHP scripts fail or run too long. Free for up to 5 monitors.

Start Monitoring Free