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() and hasBeenInterruptedSince()

   [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() and hasBeenInterruptedSince()
=======================================================================================================

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

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

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer() and hasBeenInterruptedSince()](https://cdn.msaied.com/767/f058bebd1a0c6f504e123afc581fbd66.png) 

  On this page +1. [The Problem with Running schedule:run on Multiple Servers](#the-problem-with-running-codescheduleruncode-on-multiple-servers)
2. [Schedule::alwaysOnOneServer() — Apply the Lock Globally](#codeschedulealwaysononeservercode-apply-the-lock-globally)
3. [Closure Tasks Must Have a Name](#closure-tasks-must-have-a-name)
4. [Shared Cache Is Required](#shared-cache-is-required)
5. [Schedule::hasBeenInterruptedSince() — Graceful Deploy Interruptions](#codeschedulehasbeeninterruptedsincecode-graceful-deploy-interruptions)
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 something like clearing a local file cache, but it is a real problem for a task that sends a daily report email — your users would receive it multiple times.

Laravel has long offered `onOneServer()` to solve this. The first server to claim the task takes a distributed lock in the cache; the others skip it. The catch is that you have to remember to add the call to every task, and a missed one 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()` — Apply the Lock Globally
---------------------------------------------------------

Contributed by [Jack Bayliss](https://github.com/jackbayliss), the new `Schedule::alwaysOnOneServer()` method turns on the single-server lock for every task in your schedule at once. Call it once in your `AppServiceProvider`:

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

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

```

Because the flag is applied when the schedule reads its events, tasks defined later in `routes/console.php` are covered automatically — including ones added after the `alwaysOnOneServer()` call. It is off by default, and you can disable it again with `Schedule::alwaysOnOneServer(false)`.

### Closure Tasks Must Have a Name

The lock key is derived from the task's name. Artisan commands already have one, but closure tasks only have a name if you call `->name()` explicitly. `alwaysOnOneServer()` silently skips unnamed closures — they will still run on every server — so audit your closure tasks when you enable the flag:

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

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

```

### Shared Cache Is Required

The lock lives in the cache, so every server must point at the same store (Redis, Memcached, DynamoDB, or the database driver). The `file` and `array` drivers are local to each server and will not work. If your application's 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'); // shared store from config/cache.php
}

```

`useCache()` only affects scheduler locks (`onOneServer()` and `withoutOverlapping()`); the rest of your application keeps using its default store.

---

`Schedule::hasBeenInterruptedSince()` — Graceful Deploy Interruptions
---------------------------------------------------------------------

Before 13.35, `php artisan schedule:interrupt` set a flag that only `schedule:run` checked. A long command started with `runInBackground()` ran in its own process and never saw the flag, so it either finished on old code or was killed mid-run.

Contributed by [cyppe](https://github.com/cyppe), the new approach stores the *time* of the interrupt — the same pattern `queue:restart` uses for workers. A long-running command can now call `$schedule->hasBeenInterruptedSince($startedAt)` between batches and exit cleanly:

```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 a previous 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

```

> **Note:** The interrupt time is stored in the *default* cache store, not the store passed to `useCache()`. On a multi-server setup, the default store must be shared. Calling `Schedule::withoutInterruptionPolling()` disables the cache lookups and causes `hasBeenInterruptedSince()` to always return `false`.

---

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

- `Schedule::alwaysOnOneServer()` applies a distributed lock to every task globally — no more per-task `onOneServer()` calls.
- Unnamed closure tasks are silently skipped by the global lock; always call `->name()` on closures.
- A shared cache store (Redis, database, etc.) is mandatory for any single-server locking to work.
- `Schedule::useCache('redis')` scopes the shared store to scheduler locks only, leaving your app cache untouched.
- `hasBeenInterruptedSince($startedAt)` lets background commands exit gracefully between batches when a deploy triggers `schedule:interrupt`.
- Both features shipped in Laravel 13.35 (PRs [\#61789](https://github.com/laravel/framework/pull/61789) and [\#61764](https://github.com/laravel/framework/pull/61764)).

---

*Source: [Running Laravel's Scheduler on Multiple Servers — Laravel News](https://laravel-news.com/laravel-scheduler-multiple-servers)*

- [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)
- [Deploy](https://www.msaied.com/articles?search=Deploy)

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

  What cache drivers are compatible with Laravel's single-server scheduler locking?Any shared cache driver works: Redis, Memcached, DynamoDB, or the database driver. The `file` and `array` drivers are local to each server, so each server would hold its own lock and every server would still run the task.

   Why do unnamed closure tasks still run on every server even when alwaysOnOneServer() is enabled?The lock key is derived from the task's name. Closure tasks have no name unless you call `-&gt;name()` on them. Because `onOneServer()` throws an exception on unnamed closures, `alwaysOnOneServer()` skips them silently instead. Add `-&gt;name('your-task-name')` to each closure task to bring it under the global lock.

   How does hasBeenInterruptedSince() differ from the old schedule:interrupt behavior?Previously, `schedule:interrupt` set a short-lived flag that only `schedule:run` checked, so background processes started with `runInBackground()` never saw it. In Laravel 13.35, the interrupt stores a timestamp instead. A long-running command can call `hasBeenInterruptedSince($startedAt)` between batches; it returns `true` only for an interrupt that happened at or after the command's own start time, preventing stale interrupts from stopping fresh runs.

   ![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 with Running schedule:run on Multiple Servers](#the-problem-with-running-codescheduleruncode-on-multiple-servers)
2. [Schedule::alwaysOnOneServer() — Apply the Lock Globally](#codeschedulealwaysononeservercode-apply-the-lock-globally)
3. [Closure Tasks Must Have a Name](#closure-tasks-must-have-a-name)
4. [Shared Cache Is Required](#shared-cache-is-required)
5. [Schedule::hasBeenInterruptedSince() — Graceful Deploy Interruptions](#codeschedulehasbeeninterruptedsincecode-graceful-deploy-interruptions)
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/766/d77b7cfbd66d41e2a67e814e344f2694.png) Laravel · 3 min read### Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer and hasBeenInterruptedSince

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

  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)
