Laravel Scheduler on Multiple Servers (Laravel 13.35) | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. [Laravel](https://msaied.com/articles?category=laravel)
6. /
7. Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()

   [Laravel](https://msaied.com/articles?category=laravel) [Tips &amp; Tricks](https://msaied.com/articles?category=tips-tricks) 

 Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()
=========================================================================

 Laravel 13.35 adds Schedule::alwaysOnOneServer() to prevent duplicate task runs across servers, and Schedule::hasBeenInterruptedSince() to let long-running commands stop cleanly during a deploy.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 8 Oct 2026 · Updated 10 Oct 2026 · 4 min read

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()](https://cdn.msaied.com/761/6a7f8db8e2e564efda9852eac56f6801.png) 

  On this page +1. [The Problem with Running schedule:run on Multiple Servers](#the-problem-with-running-schedulerun-on-multiple-servers)
2. [Schedule::alwaysOnOneServer()](#schedulealwaysononeserver)
3. [Name Your Closure Tasks](#name-your-closure-tasks)
4. [Shared Cache Is Required](#shared-cache-is-required)
5. [Schedule::hasBeenInterruptedSince()](#schedulehasbeeninterruptedsince)
6. [Key Takeaways](#key-takeaways)

 The Problem with Running schedule:run on Multiple Servers
---------------------------------------------------------

When you run php artisan schedule:run on more than one server, every scheduled task fires once per server. That is fine for a task that clears a local cache, but it is a real problem for a task that sends a daily report email — your users receive it multiple times.

Laravel has long offered onOneServer() to solve this. The first server to start the task takes a distributed lock in the cache; the other servers skip that run. The catch is that you have to remember to add it to every new task, and a missed call only surfaces when the duplicates appear in production.

Laravel 13.35 ships two improvements that address this and a related deploy-time problem.

Schedule::alwaysOnOneServer()
-----------------------------

Contributed by Jack Bayliss, Schedule::alwaysOnOneServer() applies the one-server constraint to every task in the schedule automatically. Call it once in your AppServiceProvider:

```php
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Schedule;

public function boot(): void
{
    Schedule::alwaysOnOneServer();
}

```

The flag is applied when the schedule reads its events, so tasks defined later in routes/console.php are covered. It is off by default, and Schedule::alwaysOnOneServer(false) turns it back off.

### Name Your Closure Tasks

The lock key is derived from the task name. Command tasks have a name automatically; closure tasks only have one if you call -&gt;name(). Without a name, alwaysOnOneServer() silently skips the closure and it runs on every server:

```php
// Runs on every server — no name, so the lock cannot be built
Schedule::call(fn () => Report::sendDaily())->daily();

// Runs on one server
Schedule::call(fn () => Report::sendDaily())
    ->name('send-daily-report')
    ->daily();

```

There is no warning when a closure is skipped, so audit your schedule for unnamed closures before enabling the flag.

### Shared Cache Is Required

The lock lives in the cache, so every server must share the same cache store (Redis, Memcached, DynamoDB, or the database driver). The file and array stores are local to each server and will not work.

If your default CACHE\_STORE is local, point the scheduler at a shared store with Schedule::useCache():

```php
public function boot(): void
{
    Schedule::alwaysOnOneServer();
    Schedule::useCache('redis');
}

```

useCache() only affects the scheduler locks. The rest of your application keeps using its default store.

Schedule::hasBeenInterruptedSince()
-----------------------------------

A deploy script often runs php artisan schedule:interrupt before switching to the new release. Before 13.35, only schedule:run checked the interrupt flag; a command started with runInBackground() ran in its own process and never saw it.

Contributed by cyppe, the new approach stores the time of the interrupt. A long-running command can now check hasBeenInterruptedSince() between batches of work:

```php
use Illuminate\Console\Command;
use Illuminate\Console\Scheduling\Schedule;

class SyncInventory extends Command
{
    protected $signature = 'inventory:sync';

    public function handle(Schedule $schedule): int
    {
        $startedAt = now();

        foreach (Product::lazyById(500) as $product) {
            if ($schedule->hasBeenInterruptedSince($startedAt)) {
                $this->info('Interrupted by a deploy, stopping.');
                return self::SUCCESS;
            }
            $this->syncProduct($product);
        }

        return self::SUCCESS;
    }
}

```

The method returns true only for an interrupt that occurred at or after the time you pass in, so a stale interrupt from an earlier deploy does not stop a fresh run.

A typical deploy script now looks like this:

```bash
php artisan schedule:interrupt
# switch release, run migrations, etc.
php artisan queue:restart

```

Key Takeaways
-------------

- Schedule::alwaysOnOneServer() replaces per-task onOneServer() calls for the entire schedule.
- Unnamed closure tasks are silently skipped — always call -&gt;name() on closures.
- Every server must share the same cache store; use Schedule::useCache('redis') if the default store is local.
- Schedule::hasBeenInterruptedSince() lets background commands stop cleanly between batches during a deploy.
- The interrupt timestamp is stored in the default cache store, which must be shared across servers.
- Both features shipped in Laravel 13.35.

- [Laravel](https://msaied.com/articles?search=Laravel)
- [Scheduler](https://msaied.com/articles?search=Scheduler)
- [Laravel 13.35](https://msaied.com/articles?search=Laravel%2013.35)
- [Multiple Servers](https://msaied.com/articles?search=Multiple%20Servers)
- [Task Scheduling](https://msaied.com/articles?search=Task%20Scheduling)

 Frequently asked questions 
---------------------------

  What cache stores work with alwaysOnOneServer() in a multi-server setup?You need a shared cache store that all servers can access, such as Redis, Memcached, DynamoDB, or the database driver. The file and array stores are local to each server and will not prevent duplicate task runs. Use Schedule::useCache('redis') in your AppServiceProvider if your default cache store is not already shared.

   Why do unnamed closure tasks still run on every server even with alwaysOnOneServer() enabled?The one-server lock key is built from the task name. Closure tasks have no name unless you call -&gt;name() on them. When alwaysOnOneServer() cannot build a lock key it silently skips the constraint, so the closure runs on every server. Add -&gt;name('your-task-name') to any closure task you want covered.

   How does Schedule::hasBeenInterruptedSince() differ from the old schedule:interrupt behavior?Previously, schedule:interrupt set a flag that only schedule:run checked, so background commands started with runInBackground() never saw it. In Laravel 13.35 the interrupt stores a timestamp instead. A long-running command can call hasBeenInterruptedSince() between batches; it returns true only for an interrupt that occurred at or after the command own start time, so stale interrupts from earlier deploys are ignored.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://msaied.com#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articlePostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Mess](https://msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-mess) [Next articleEloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination](https://msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination)  

   On this page
-------------

1. [The Problem with Running schedule:run on Multiple Servers](#the-problem-with-running-schedulerun-on-multiple-servers)
2. [Schedule::alwaysOnOneServer()](#schedulealwaysononeserver)
3. [Name Your Closure Tasks](#name-your-closure-tasks)
4. [Shared Cache Is Required](#shared-cache-is-required)
5. [Schedule::hasBeenInterruptedSince()](#schedulehasbeeninterruptedsince)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/757/f990951a1a0e14b4fece312bce644c29.png)  · 4 min read### Read/Write Splitting and Sticky Reads in Laravel: A Production Guide

9 Oct 2026 ](https://msaied.com/articles/readwrite-splitting-and-sticky-reads-in-laravel-a-production-guide-1) [ ![](https://cdn.msaied.com/756/69efdf9ac9ec1e1f61378055f1cd6e92.png)  · 4 min read### Eloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination

9 Oct 2026 ](https://msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination) [ ![](https://cdn.msaied.com/760/fce56c9f7c15f70c3313c8faf2c9f4c1.png) Laravel · 3 min read### Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()

8 Oct 2026 ](https://msaied.com/articles/running-laravels-scheduler-on-multiple-servers-with-alwaysononeserver-2) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
