Laravel has four credible ways to schedule work on a clock. Say you ship a SaaS that needs nightly Stripe reconciliation, an hourly digest email, and a weekly cleanup that purges expired carts. You can lean on the built-in Laravel Scheduler with schedule:run, run supervised queue workers, use a platform scheduler like Forge or Vapor, or expose an HTTP endpoint and let an external cron service like Crontap hit it on cadence.
This guide walks through all four side by side, with code. For the broader PHP picture (cPanel, Composer, framework-agnostic patterns) see PHP cron jobs.
Path 1: Laravel Scheduler + schedule:run
The Laravel-idiomatic path. You define schedules in PHP and one crontab line drives the whole thing:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1That line fires every minute. Laravel checks which of your scheduled commands are due and dispatches them. The definitions live in app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
$schedule->command('stripe:reconcile')
->dailyAt('02:15')
->withoutOverlapping()
->runInBackground();
$schedule->command('digest:send')
->hourly()
->evenInMaintenanceMode();
$schedule->command('carts:purge')->weeklyOn(0, '03:00');
}Three modifiers worth knowing:
->withoutOverlapping()skips the next tick if the previous run is still going. The default lock lasts 24 hours, override withwithoutOverlapping(60)for 60 minutes.->runInBackground()lets the scheduler hand off long commands and keep polling. Without it a slow job can delay other minute-cadence schedules.->evenInMaintenanceMode()keeps critical schedules running whileartisan downis active.
In Laravel 11.x and 12.x you can also define schedules in routes/console.php instead of Kernel.php. The mechanics are identical, the host crontab still needs to fire schedule:run every minute.
Path 2: Supervised long-running workers
php artisan queue:work is not the scheduler. It is the worker that drains queued jobs. The two coexist: a scheduled command can dispatch(new RebuildSearchIndex()), the worker picks the job up and runs it.
Supervisor is the standard way to keep workers alive on a VM:
[program:laravel-worker]
command=php /var/www/app/artisan queue:work redis --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4Laravel Horizon wraps the same idea with a dashboard, auto-scaling, and tag-based monitoring. Pick Horizon when you have Redis and want visibility, plain Supervisor when you want fewer moving parts.
The scheduler still needs Path 1 or Path 4 to fire. Workers handle the dispatched payloads, they do not fire schedules themselves.
Path 3: Platform cron
Most managed Laravel hosts ship a scheduler so you do not own a crontab:
- Laravel Forge has a built-in Scheduler tab that writes the
* * * * *entry forschedule:runon the server it provisions. You stop editing crontabs by hand. - Laravel Vapor runs on AWS Lambda. Schedules live in
vapor.ymlas cron expressions per environment, Vapor wires them to EventBridge under the hood. - Heroku ships Heroku Scheduler with 10 minute, hourly, and daily slots, see cron jobs for Heroku for the Laravel specifics and a comparison with Heroku Scheduler alternatives.
- Render has Cron Jobs that run
php artisan ...on a schedule, billed per run.
One Octane note: the scheduler is independent of Octane. Octane keeps your app booted between requests for speed, the cron entry for schedule:run still runs as a separate PHP process every minute.
Path 4: External HTTP cron
Schedule your Laravel jobs from outside the box. Free forever tier with one schedule. Try Crontap →
Expose an Artisan command behind an authenticated HTTP route and let Crontap hit it on cadence. The host crontab becomes optional in this model.
// routes/api.php
Route::post('/internal/cleanup', function (Request $request) {
if ($request->bearerToken() !== config('services.cron.secret')) {
abort(401);
}
Artisan::call('app:cleanup');
return response()->noContent();
});In Crontap: POST https://api.yourapp.com/internal/cleanup, header Authorization: Bearer <secret>, cron 15 2 * * *, timezone America/New_York. Pro is $3.25/mo annual flat for unlimited HTTP schedules at minute cadence on a 1-minute floor.
You get retries on 5xx, failure alerts to Slack or email, per-schedule IANA timezones, and no always-on PHP process whose only job is to run schedule:run.
How to make a Laravel cron idempotent
Cron fires whether yesterday's run finished or not. For Scheduler jobs, chain withoutOverlapping(). For the controller pattern, use Cache::lock():
Route::post('/internal/cleanup', function () {
Cache::lock('cleanup', 600)->block(0, function () {
Artisan::call('app:cleanup');
});
return response()->noContent();
});If two invocations race, the second exits cleanly instead of double-charging customers.
FAQ
What's the difference between schedule:run and queue:work?
schedule:run checks the schedule and dispatches due commands, fired by cron every minute. queue:work is a long-running worker that drains the queue. Most non-trivial apps run both.
How do I run a scheduled command on shared hosting?
If your host allows crontab edits, add the * * * * * php artisan schedule:run line. If not, use Path 4: hit an authenticated route from Crontap and skip the crontab entirely.
Does Forge replace the need for the cron entry?
Forge writes the cron entry for you. Under the hood it still installs the * * * * * line that runs schedule:run on the provisioned server.
Should I switch to external cron if I'm on Forge?
If your Forge server only exists to run schedule:run plus a small admin task, external HTTP cron lets you downsize or skip the server. If the Forge box already serves the app, the built-in scheduler is fine.
Related on Crontap
- PHP cron jobs. The framework-agnostic PHP picture.
- Cron troubleshooting. When
schedule:runruns but your command does not. - Cron job monitoring. Alerts for missed and failing schedules.
- Cron job not running?. Debug checklist.
- Cron jobs for Heroku. Heroku + Laravel specifics.
