PHP Cron Scheduling — A Complete Guide
PHP's scheduling story splits between framework-managed scheduling (Laravel's `Schedule` facade, Symfony's Scheduler) and the older pattern of system crontab invoking PHP CLI scripts. Both work; the framework approaches are better for ergonomics and observability, system cron is better for low-overhead one-liners.
Updated
The landscape
| Tool | What it does | Best for |
|---|---|---|
| Laravel Scheduler | Built-in scheduler for Laravel apps | Laravel apps (clear choice) |
| Symfony Scheduler | Built-in scheduler for Symfony 6.3+ | Symfony apps |
| dragonmantank/cron-expression | Cron expression parsing library | Custom schedulers, validation, next-run display |
System crontab + php CLI | Traditional cron calling PHP scripts | Standalone PHP, simple needs |
| Workerman, Swoole timers | Long-running PHP process schedulers | Real-time PHP apps |
For Laravel and Symfony users, the choice is obvious — use the built-in. For other PHP frameworks (Yii, CakePHP, plain PHP) or standalone scripts, system cron calling php /path/to/script.php is the standard pattern.
Laravel Scheduler
Laravel ships with a fluent scheduling DSL. As of Laravel 11 (2024), schedules live in routes/console.php using the Schedule facade — the old app/Console/Kernel.php schedule() method no longer exists in new apps:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')
->dailyAt('09:00')
->timezone('America/New_York')
->emailOutputOnFailure('alerts@example.com');
Schedule::command('cache:prune')
->everyFiveMinutes()
->withoutOverlapping();
Schedule::call(function () {
// closure-based task
})->hourly();
Schedule::job(new GenerateReportJob)
->cron('0 9 * * MON-FRI');
(On Laravel 10 and earlier, the same method chains go inside the schedule(Schedule $schedule) method in app/Console/Kernel.php instead.)
This generates schedule entries, but Laravel doesn’t run them itself — you add one crontab entry that ticks the scheduler every minute:
* * * * * cd /path/to/laravel && php artisan schedule:run >> /dev/null 2>&1
schedule:run reads your defined schedule, figures out which tasks should fire in the current minute, and runs them. So you only need to edit Laravel code, never the crontab.
Key methods worth knowing:
->cron('* * * * *')— raw cron expression->everyMinute(),->everyFiveMinutes(),->hourly(),->dailyAt('13:30'),->weeklyOn(1, '9:00')— fluent shortcuts->withoutOverlapping()— cache-based lock; skips if previous fire still running->onOneServer()— for multi-server deploys, runs on exactly one server (requires a shared cache like Redis)->runInBackground()— don’t wait for the task to finish before scheduling the next one->emailOutputTo(...),->emailOutputOnFailure(...)— pipe stdout to email->before(fn() => ...),->after(fn() => ...)— hooks->between('9:00', '17:00'),->weekdays(),->weekends()— time/day constraints
For local development, php artisan schedule:work runs the scheduler in the foreground without needing crontab — useful for testing.
Symfony Scheduler
Symfony Scheduler (introduced in Symfony 6.3) is built on top of Symfony Messenger. Define a ScheduleProvider:
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
#[AsSchedule('default')]
class MainSchedule implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
->add(
RecurringMessage::cron('0 9 * * MON-FRI', new SendDailyDigestMessage()),
RecurringMessage::every('5 minutes', new RefreshCacheMessage()),
);
}
}
Then create message handlers and run the consumer:
php bin/console messenger:consume scheduler_default
The consumer is a long-running process — deploy it like any other queue worker (supervisor, systemd, k8s).
Differences from Laravel’s approach:
- Long-running consumer, not a crontab tick. Symfony Scheduler runs continuously and dispatches messages at the right times. No need for a separate crontab entry.
- Built on Messenger. Scheduled tasks dispatch into the same message bus your async jobs use. Easier to scale, monitor, and observe.
- More trigger types. Cron, periodic intervals, and callback-based triggers all supported via the same API.
dragonmantank/cron-expression
The standard PHP library for parsing cron expressions. Used internally by Laravel Scheduler and many other tools.
composer require dragonmantank/cron-expression
use Cron\CronExpression;
$cron = new CronExpression('*/5 * * * *');
// Next run from now
$next = $cron->getNextRunDate();
echo $next->format('c');
// Validate a user-submitted expression
if (CronExpression::isValidExpression($userInput)) {
// safe to use
}
// Get the next 10 runs. The second arg is a skip count (0 = the next run),
// so start at 0 — starting at 1 would skip the first upcoming run.
for ($i = 0; $i < 10; $i++) {
echo $cron->getNextRunDate('now', $i)->format('c') . "\n";
}
Use cron-expression when you need to:
- Parse user-submitted cron expressions for validation
- Display “next run” times in an admin UI
- Compute frequencies or schedule durations
- Build a custom scheduling layer
The library accepts standard 5-field Unix cron — see the Unix cron dialect reference. For Quartz, EventBridge, or other dialects, you’ll need to convert first.
System cron + PHP CLI
For non-framework PHP or standalone scripts, the traditional pattern is fine:
0 0 * * * /usr/bin/php /var/www/scripts/nightly-cleanup.php >> /var/log/cleanup.log 2>&1
*/5 * * * * /usr/bin/php /var/www/scripts/poll-api.php
Tips for PHP CLI cron scripts:
- Use absolute paths.
/usr/bin/phpand/var/www/scripts/..., not bare names. - Set the PATH at the top of the crontab so anything the script shells out to is findable.
- Redirect stderr to stdout, both to a logfile.
>> log 2>&1is the standard idiom. - Use
flockto prevent overlapping runs:*/5 * * * * /usr/bin/flock -n /tmp/poll.lock /usr/bin/php /var/www/poll.php - Check exit codes. Cron sends an email (if MAILTO is set) when the command exits non-zero. Make sure your script returns 0 on success and non-zero on failure.
Most importantly: write a small wrapper script that handles common PHP CLI concerns (chdir, autoload, error reporting), then have your scheduled tasks require it. Avoids duplicating boilerplate across every cron script.
Choosing the right approach
- Laravel app? → Laravel Scheduler. No real alternative makes sense.
- Symfony 6.3+ app? → Symfony Scheduler.
- Plain PHP or older framework? → System cron + PHP CLI scripts.
- Need to parse cron expressions for validation/display? → dragonmantank/cron-expression.
- Need SLA-grade reliability? → Move scheduling out of PHP. Use Kubernetes CronJob or AWS EventBridge Scheduler to invoke a PHP HTTP endpoint on a schedule.
The “external scheduler invokes a PHP endpoint” pattern is the cleanest production-grade option — your PHP app stays stateless, the scheduler handles failure modes, and the scheduling decision is decoupled from your application code.
Frequently asked questions
Which PHP scheduling tool should I use?
Does Laravel Scheduler actually replace cron?
How do I prevent overlapping runs in Laravel Scheduler?
What's Symfony Scheduler vs Laravel Scheduler?
Can I run sub-minute schedules in PHP?
Related
Every 5 Minutes
`*/5 * * * *` — common PHP scheduled-job interval.
ToolCron Parser
Verify your cron expression before adding it to Laravel or Symfony.
GuideNode.js Cron Jobs
The Node equivalents — node-cron, agenda, BullMQ.
GuidePython Cron Scheduling
The Python equivalents — APScheduler, Celery Beat.
GuideKubernetes CronJob
Run scheduled PHP work as a K8s CronJob for SLA-grade reliability.