Back to guides

Guides · April 21, 2026

Laravel cron jobs: Scheduler, Artisan, and external cron (2026 walkthrough)

Four ways to schedule Laravel in production, with code for each. Pick the built-in Scheduler with `schedule:run`, supervised queue workers, platform cron on Forge, Vapor, or Heroku, or external HTTP cron hitting an Artisan command when you want retries and alerts without an always-on PHP process.
crontap.com / guides
Four ways to schedule Laravel: the built-in Scheduler with `schedule:run`, supervised queue workers, platform cron (Forge, Vapor, Heroku), and external HTTP cron hitting an Artisan command. Code for each.

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>&1

That 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:

  1. ->withoutOverlapping() skips the next tick if the previous run is still going. The default lock lasts 24 hours, override with withoutOverlapping(60) for 60 minutes.
  2. ->runInBackground() lets the scheduler hand off long commands and keep polling. Without it a slow job can delay other minute-cadence schedules.
  3. ->evenInMaintenanceMode() keeps critical schedules running while artisan down is 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=4

Laravel 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:

  1. Laravel Forge has a built-in Scheduler tab that writes the * * * * * entry for schedule:run on the server it provisions. You stop editing crontabs by hand.
  2. Laravel Vapor runs on AWS Lambda. Schedules live in vapor.yml as cron expressions per environment, Vapor wires them to EventBridge under the hood.
  3. 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.
  4. 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

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.