Laravel cron job setup for the scheduler
Laravel needs one crontab line running php artisan schedule:run every minute. How to add it on a server, Forge, Ploi or Docker, and why it may not run.
On this page
Laravel needs exactly one cron job: a crontab line that runs php artisan schedule:run every minute. Each run checks the tasks in routes/console.php and starts the ones due at that minute; you never add a cron line per task.
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Everything below was run on Laravel 13.42 and checked against the Laravel 13.x scheduling docs on 10 Nov 2026. It applies to Laravel 11 and 12 too.
Add the crontab line on a server
-
Log in as the user that owns the app and runs PHP-FPM, often
www-data,forgeorploi. Notroot: files the scheduler creates, such as the day's log file, would belong to root and the web server couldn't write to them. -
Find the PHP binary and the project path:
Terminalwhich php pwd -
Open the crontab with
crontab -eand add one line, with absolute paths:* * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /dev/null 2>&1Replace
/var/www/example.comwith your project directory and/usr/bin/phpwith the output ofwhich php. Cron runs with a minimalPATH, so a plainphpcan find another PHP version than your shell does, or none. -
Check it was saved:
crontab -l. For another user:sudo crontab -u www-data -l.
The five * mean every minute of every hour of every day. The server's timezone doesn't matter for this line, because it runs every minute anyway; task times come from Laravel (see timezones below).
>> /dev/null 2>&1 throws away the output. While you set things up, send it to a file instead, so a broken run leaves a trace:
* * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /var/www/example.com/storage/logs/schedule.log 2>&1
Check that it runs
schedule:list shows every task with its cron expression and next run:
php artisan schedule:list
* * * * * php artisan reports:send .. Next Due: 45 seconds from now
30 2 * * * php artisan backup:run .. Next Due: 9 hours from now
schedule:run by hand shows what it starts (this is a Mac with Homebrew PHP). The scheduler runs each task with the same PHP binary as schedule:run itself, which the second line shows:
2026-11-10 16:45:14 Running ['artisan' reports:send] .. 455.41ms DONE
⇂ '/opt/homebrew/Cellar/php/8.5.10/bin/php' 'artisan' reports:send > '/dev/null' 2>&1
php artisan schedule:test lets you pick one task and run it now, whatever its schedule.
Laravel Forge, Ploi and Laravel Cloud
- Laravel Forge: turn on the Laravel Scheduler toggle of the site. Forge creates the every-minute job with the site's PHP version (Forge docs). Jobs you add yourself live under Processes → Scheduler of the server or site.
- Ploi: on the server, open Cron jobs and add a job with the command
php8.5 /home/ploi/example.com/artisan schedule:run, userploi, frequency every minute (Ploi docs). Use your site's PHP version and path. - Laravel Cloud: click the environment's App cluster, turn on the Scheduler toggle, then save and redeploy. There is no crontab line to write. With more than one replica, use
onOneServer()or tasks run on every replica (Cloud docs).
The three docs pages were read on 10 Nov 2026.
Docker and Kubernetes
Containers usually have no cron daemon. Two common setups:
-
A scheduler container running
schedule:work. Same image and environment as the app, withphp artisan schedule:workas its command. It stays in the foreground and calls the scheduler every minute. Run exactly one of these, or tasks run once per container.YAML# docker-compose.yml scheduler: image: your-app-image command: php artisan schedule:work env_file: .env restart: unless-stopped -
A Kubernetes CronJob with schedule
* * * * *and commandphp artisan schedule:run. Each minute starts a new pod; setconcurrencyPolicy: Forbidso slow runs don't pile up.
The Laravel docs present schedule:work for local development. In a container it does the same job as cron, with one difference: when the process dies, nothing runs until your orchestrator restarts it.
Tasks that run longer, or on several servers
Three options decide what happens when the cron line fires again before a task is done, or fires on more than one server:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:send')->everyFiveMinutes()->withoutOverlapping(10);
Schedule::command('invoices:sync')->hourly()->onOneServer();
Schedule::command('analytics:report')->daily()->runInBackground();
withoutOverlapping()skips a run while the previous one still holds a cache lock. The lock expires after 24 hours by default; pass minutes, as above, so a killed process doesn't block the task for a day.onOneServer()lets only the first server that grabs the lock run the task. It needs adatabase,memcached,dynamodborredisdefault cache that all servers share. Scheduled closures also need a->name('…').runInBackground()starts acommandorexectask without waiting for it. Tasks due at the same minute otherwise run one after another, so one slow task delays the rest.
Sub-minute tasks (everyTenSeconds() and the like) keep schedule:run alive for the whole minute. Add php artisan schedule:interrupt to your deploy script, or the old run keeps using the old code until the minute ends.
Timezones
Laravel decides when a task is due in the app's timezone, timezone in config/app.php (UTC by default). Set another one per task, or for all tasks with schedule_timezone:
// config/app.php
'timezone' => 'UTC',
'schedule_timezone' => 'Europe/Amsterdam',
Schedule::command('backup:run')->dailyAt('02:30')->timezone('Europe/Amsterdam');
On the nights the clocks change, a task in a daylight saving timezone can run twice or not at all; the Laravel docs advise against timezone scheduling for that reason. Keep the app in UTC, and don't put important tasks between 02:00 and 03:00 local time.
Why the scheduler isn't running
Most "the scheduler stopped" reports come down to one of these:
| Symptom | Likely cause | Check |
|---|---|---|
| Nothing runs, no errors anywhere | No crontab line on this server, or it's in another user's crontab | crontab -l as the app user |
| Works by hand, not from cron | Cron finds another php, or the cd path is wrong | Use absolute paths; log output to a file instead of /dev/null |
| Ran until the last deploy | New server or container without cron; deploy left the app in maintenance mode | php artisan schedule:list on the server; tasks skip php artisan down unless they have evenInMaintenanceMode() |
| One task never runs | A stuck withoutOverlapping lock after a crash | php artisan schedule:clear-cache |
| A task runs on every server | onOneServer() without a shared cache | Your default cache store |
| Runs at the wrong hour | App timezone or daylight saving time | php artisan schedule:list |
| Nothing runs since someone paused it | php artisan schedule:pause | php artisan schedule:continue |
The scheduler monitoring guide reproduces the stuck lock and the "it failed but said DONE" case, and shows how to catch each.
Know when it stops
A crontab line can disappear with a server move and nothing complains: no task runs, so no task fails. The usual fix is a heartbeat per task: ->pingOnSuccess($url) to a cron monitoring service, which alerts when a ping is missing.
We make Lampwick, which checks the schedule without heartbeats: the Laravel package reports every scheduled task run, and Schedule health learns each task's cron and timezone from its runs. When a task misses its run you get an alert, and when no task runs at all you get one "scheduler not running" alert for the project instead of one per task. Details are in the Schedule health docs and on the cron monitoring page.
Next step
Add the crontab line, run php artisan schedule:list, then set up monitoring for the scheduler so you hear about the night it doesn't run.