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://www.msaied.com) - [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [About](https://www.msaied.com#about)

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

Menu
----

Close 

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

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

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

   [Laravel](https://www.msaied.com/articles?category=laravel) [Tips &amp; Tricks](https://www.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 on deploy.

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

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()](https://cdn.msaied.com/758/1d3bc21cbf8aabd81b1a721134f37007.png) 

  On this page +1. [The Problem: Duplicate Scheduled Tasks Across Servers](#the-problem-duplicate-scheduled-tasks-across-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: Duplicate Scheduled Tasks Across Servers
-----------------------------------------------------

When you run schedule:run on more than one server, every scheduled task fires once per server. Clearing a local cache is fine that way, but sending a daily report email three times is not. Laravel has long offered onOneServer() to take a cache lock and let only the first server run a given task, but you had to remember to add it to every single task definition.

Laravel 13.35 ships two improvements that make multi-server scheduling much easier to manage.

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 service provider:

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

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

```

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

### Name Your Closure Tasks

The lock key is built from the task name. Artisan 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
Schedule::call(fn () => Report::sendDaily())->daily();

// Runs on one server - name provided
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. Every server must point at the same cache store such as Redis, Memcached, DynamoDB, or the database. The file and array drivers 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 scheduler locks; the rest of your application keeps its default store.

If some tasks genuinely need to run on every server, leave alwaysOnOneServer() off and call -&gt;onOneServer() only on the tasks that should run once.

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

Before 13.35, php artisan schedule:interrupt set a flag that only schedule:run checked. A command started with runInBackground() ran in its own process and never saw the flag, so it kept running on old code through a deploy.

Contributed by cyppe, the new approach stores the time of the interrupt, mirroring how queue:restart works. A long-running command can now poll that time against its own start time and stop gracefully between batches:

```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;
    }
}

```

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

A typical deploy script now looks like:

```bash
php artisan schedule:interrupt
php artisan queue:restart

```

The interrupt time is stored in the default cache store, not the one passed to useCache(). That default store must be shared across servers. Calling Schedule::withoutInterruptionPolling() disables polling, causing hasBeenInterruptedSince() to always return false.

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

- Schedule::alwaysOnOneServer() replaces per-task -&gt;onOneServer() calls with a single global setting.
- Closure tasks must have a -&gt;name() to be covered; unnamed closures are silently skipped.
- A shared cache store such as Redis or the database is mandatory for any one-server locking to work.
- Schedule::useCache('redis') scopes the shared store to scheduler locks only.
- Schedule::hasBeenInterruptedSince() lets background commands stop cleanly on deploy.
- The interrupt timestamp approach matches queue:restart, making deploy scripts consistent.

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

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

  What is Schedule::alwaysOnOneServer() in Laravel 13.35?It is a method introduced in Laravel 13.35 that applies the one-server constraint to every scheduled task globally, so you no longer need to add -&gt;onOneServer() to each individual task. Call it once in your AppServiceProvider boot method.

   Why do unnamed closure tasks still run on every server even with alwaysOnOneServer() enabled?The one-server lock key is derived from the task name. Closure tasks have no name unless you call -&gt;name() on them. Without a name, alwaysOnOneServer() cannot build a lock key, so it skips those closures and they run on every server without any warning.

   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() were unaffected. In Laravel 13.35, the interrupt stores a timestamp instead. Long-running commands can call hasBeenInterruptedSince() between batches to detect a deploy interrupt and exit cleanly.

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

[Mohamed Said](https://www.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://www.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://www.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://www.msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination)  

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

1. [The Problem: Duplicate Scheduled Tasks Across Servers](#the-problem-duplicate-scheduled-tasks-across-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://www.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://www.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://www.msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination) [ ![](https://cdn.msaied.com/755/2afbdc0a82de6bd61d3739c09554dda0.png)  · 4 min read### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Mess

8 Oct 2026 ](https://www.msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-mess) 

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

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

 [Discuss your project ↗](https://www.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://www.msaied.com)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.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://www.msaied.com/sitemap.xml)
