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

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

 Laravel 13.35 introduces Schedule::alwaysOnOneServer() to prevent duplicate task execution across servers, and Schedule::hasBeenInterruptedSince() to let long-running commands stop gracefully during deploys.

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

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer and hasBeenInterruptedSince](https://cdn.msaied.com/766/d77b7cfbd66d41e2a67e814e344f2694.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. For a task that clears a local cache that is fine, but for a daily report email it means your users receive duplicates. Laravel has long offered `onOneServer()` to handle this, but you had to remember to add it to every single task.

Laravel 13.35 ships two improvements that address this directly.

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

`Schedule::alwaysOnOneServer()` applies the single-server lock to every task in your schedule from one central place — your service provider:

```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 automatically. It is off by default, and you can disable it again with `Schedule::alwaysOnOneServer(false)`.

### Name Your Closure Tasks

The `onOneServer()` lock key is derived from the task name. Artisan commands have a name built in, but closure tasks only have one if you call `->name()`. With `alwaysOnOneServer()` enabled, unnamed closures are silently skipped and still run on every server. Always add a name to closure tasks:

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

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

```

### Shared Cache Is Required

The lock lives in the cache, so all servers must share the same cache store (Redis, Memcached, DynamoDB, or the database driver). If your default `CACHE_STORE` is local to each server, 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 using its default store.

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

Before Laravel 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 would either finish on old code or get killed mid-execution during a deploy.

The new approach stores the time of the interrupt. A long-running command can now compare that timestamp against its own start time and exit cleanly 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 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

```

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.

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

- `Schedule::alwaysOnOneServer()` in `AppServiceProvider::boot()` replaces per-task `->onOneServer()` calls.
- Unnamed closure tasks are silently skipped by `alwaysOnOneServer()` — always call `->name()` on them.
- All servers must share a cache store; use `Schedule::useCache('redis')` if your default store is local.
- `hasBeenInterruptedSince($startedAt)` lets background commands stop gracefully between batches during a deploy.
- The interrupt timestamp is stored in the default cache store, which must also be shared.
- Both features shipped in Laravel 13.35.

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

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

  What is the difference between onOneServer() and alwaysOnOneServer() in Laravel?onOneServer() must be chained onto each individual task to prevent it from running on more than one server. alwaysOnOneServer(), introduced in Laravel 13.35, is called once in a service provider and applies the single-server lock to every task in the schedule automatically, including tasks defined later in routes/console.php.

   Why do unnamed closure tasks still run on every server even when alwaysOnOneServer() is enabled?The lock key used by onOneServer() is derived from the task name. Closure tasks have no name unless you call -&gt;name() on them. Because alwaysOnOneServer() cannot build a lock key for an unnamed closure, it skips those tasks and they continue to run on every server. There is no warning, so you should audit all closure tasks and add -&gt;name() to each one.

   How does Schedule::hasBeenInterruptedSince() improve on the old schedule:interrupt behavior?Previously, schedule:interrupt set a flag that only the schedule:run loop checked, so background commands started with runInBackground() never saw it. In Laravel 13.35, schedule:interrupt stores the time of the interrupt instead. A long-running command can call hasBeenInterruptedSince() between batches; it returns true only if an interrupt occurred at or after the command own start time, allowing the command to exit cleanly without being killed mid-execution.

   ![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: 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://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/765/c39e5397b2a498c12d18b6f1d5a5c726.png) Laravel · 4 min read### Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer() and hasBeenInterruptedSince()

8 Oct 2026 ](https://msaied.com/articles/running-laravels-scheduler-on-multiple-servers-with-alwaysononeserver-and-hasbeeninterruptedsince-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)
