Saturday, October 10, 2026
HomePHPOperating Laravel's Scheduler on A number of Servers

Operating Laravel’s Scheduler on A number of Servers


A Laravel utility that runs schedule:run on multiple server runs every scheduled job as soon as per server. That’s advantageous for a job that clears an area cache. Nevertheless, for a job that sends a each day report e-mail that is not supreme.

Laravel 13.35 provides two adjustments for this setup. The brand new Schedule::alwaysOnOneServer() methodology runs each job on one server, and Schedule::hasBeenInterruptedSince() lets a long-running scheduled command cease when a deploy runs schedule:interrupt.

Utilizing onOneServer() for Each Activity

Laravel has had onOneServer() for a very long time. The primary server to begin the duty takes a lock within the cache, and the opposite servers skip it for that run:

Schedule::command('reviews:ship')->each day()->onOneServer();

The issue is remembering it for each new job wants the decision, and a missed one solely reveals up when the duplicates do. The workaround was wrapping the entire schedule in a bunch:

Schedule::onOneServer()->group(perform () {

Schedule::command('reviews:ship')->each day();

Schedule::command('invoices:generate')->month-to-month();

// each different job...

});

Jack Bayliss contributed Schedule::alwaysOnOneServer(), which turns it on for the entire schedule from a service supplier:

// app/Suppliers/AppServiceProvider.php

use IlluminateSupportFacadesSchedule;

 

public perform boot(): void

{

Schedule::alwaysOnOneServer();

}

The flag is utilized when the schedule reads its occasions, so duties outlined later in routes/console.php are coated, together with ones added after the decision. It’s off by default, and Schedule::alwaysOnOneServer(false) turns it again off.

Title Your Closure Duties

The onOneServer() methodology builds its lock key from the duty’s title. A command job has one from its command line, however a closure job solely has one should you name title(). Calling onOneServer() on an unnamed closure throws an exception, so alwaysOnOneServer() skips these closures as an alternative. They nonetheless run on each server:

// Runs on each server, even with alwaysOnOneServer()

Schedule::name(fn () => Report::sendDaily())->each day();

 

// Runs on one server

Schedule::name(fn () => Report::sendDaily())->title('send-daily-report')->each day();

There isn’t a warning for a skipped closure, so verify closure duties for a title() name while you flip the flag on.

A Shared Cache

The lock lives within the cache, so each server has to make use of the identical cache retailer, comparable to Redis, Memcached, DynamoDB, or the database. With the file or array retailer, every server has its personal lock and each server runs the duty.

By default, the scheduler takes its locks in your utility’s default cache retailer, the one set by CACHE_STORE. If that retailer is native to every server, for instance as a result of the app makes use of file for its personal caching, name Schedule::useCache() with the title of a shared retailer from config/cache.php. It goes in the identical boot() methodology as alwaysOnOneServer():

// app/Suppliers/AppServiceProvider.php

use IlluminateSupportFacadesSchedule;

 

public perform boot(): void

{

Schedule::alwaysOnOneServer();

 

// The 'redis' retailer from config/cache.php, shared by each server

Schedule::useCache('redis');

}

The useCache() methodology adjustments the shop for the scheduler’s locks solely, which covers onOneServer() and withoutOverlapping(). The remainder of your utility retains utilizing its default retailer. If the default retailer is already shared, comparable to redis or database on each server, you possibly can skip this name.

There isn’t a per-task opt-out. If some duties must run on each server, comparable to one which clears recordsdata on native disk, depart alwaysOnOneServer() off and name onOneServer() on the duties that ought to run as soon as.

Stopping Lengthy Instructions Throughout a Deploy

A deploy script may run php artisan schedule:interrupt earlier than it switches to the brand new launch. Earlier than 13.35, that command set a flag within the cache till the top of the present minute, and solely schedule:run checked it, to cease its loop of sub-minute duties. A command began with runInBackground() runs in its personal course of and by no means checked the flag, so a job that ran for an hour completed on the previous code or was killed partway by.

cyppe modified schedule:interrupt to retailer the time of the interrupt as an alternative, the identical manner queue:restart shops a restart time for queue staff. A protracted command can now examine that point with its personal begin time and cease between batches:

use IlluminateConsoleCommand;

use IlluminateConsoleSchedulingSchedule;

 

class SyncInventory extends Command

{

protected $signature = 'stock:sync';

 

public perform deal with(Schedule $schedule): int

{

$startedAt = now();

 

foreach (Product::lazyById(500) as $product) {

if ($schedule->hasBeenInterruptedSince($startedAt)) {

$this->data('Interrupted by a deploy, stopping.');

 

return self::SUCCESS;

}

 

$this->syncProduct($product);

}

 

return self::SUCCESS;

}

}

Schedule::command('stock:sync')->hourly()->runInBackground();

The command wants to have the ability to cease at any level it checks, and the subsequent scheduled run has to select up the remaining work. Right here, the sync can run once more over each product, so stopping midway loses nothing.

The hasBeenInterruptedSince() methodology returns true just for an interrupt at or after the time you go in, so an interrupt from an earlier deploy doesn’t cease a brand new run. The identical verify now drives schedule:run, which not stops for an interrupt despatched earlier in the identical minute.

The interrupt time is saved within the default cache retailer, not the shop handed to useCache(), so on a number of servers, the default retailer must be one all of them share. In the event you name Schedule::withoutInterruptionPolling() to avoid wasting the cache lookups, hasBeenInterruptedSince() at all times returns false.

A deploy script that alerts each the scheduler and the queue staff appears to be like like this:

php artisan schedule:interrupt

# change to the brand new launch, run migrations, and so forth

php artisan queue:restart

Additional Studying

Jack Bayliss contributed alwaysOnOneServer() in #61789, and cyppe contributed hasBeenInterruptedSince() in #61764.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments