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 during deploys.

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

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()](https://cdn.msaied.com/759/bf983fc4e6fdc02c324b1620b8621706.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. Sending a daily report email three times because you have three app servers is not acceptable. Laravel has long offered `onOneServer()` to take a cache lock and skip duplicate runs, but you had to remember to add it to every task.

**Laravel 13.35** ships two improvements that address this directly.

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

`Schedule::alwaysOnOneServer()` applies the single-server constraint to every task in your schedule at once. Call it once in `AppServiceProvider::boot()`:

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

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

```

Tasks defined later in `routes/console.php` are covered automatically. It is off by default, and you can disable it with `Schedule::alwaysOnOneServer(false)`.

### Name Your Closure Tasks

The lock key is derived from the task name. Command tasks have a name automatically; closure tasks do not unless you call `name()`.

```php
// Runs on every server — no name, lock cannot be taken
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();

```

`alwaysOnOneServer()` silently skips unnamed closures rather than throwing. Audit your closure tasks before enabling the flag.

### Shared Cache Is Required

The lock is stored in the cache. Every server must point at the same store — 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.

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

This method lets a long-running background command check whether `schedule:interrupt` was called after the command started, and exit cleanly between batches.

Previously, `schedule:interrupt` only stopped the `schedule:run` loop; commands launched with `runInBackground()` ran in their own process and never saw the flag.

Now, the interrupt stores a timestamp. A command compares that timestamp against its own start time:

```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 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:

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

```

The interrupt timestamp is stored in the default cache store, not the store passed to `useCache()`. That store must be shared across servers.

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

- `Schedule::alwaysOnOneServer()` in `AppServiceProvider::boot()` prevents duplicate task runs globally.
- Unnamed closure tasks are silently skipped — always call `name()` on closure tasks.
- A shared cache store (Redis, Memcached, DynamoDB, or database) is mandatory.
- `Schedule::useCache('redis')` redirects scheduler locks to a specific store without affecting the rest of the app.
- `hasBeenInterruptedSince($startedAt)` lets long-running background commands stop gracefully between batches when a deploy fires `schedule:interrupt`.

- [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)
- [Multiple Servers](https://www.msaied.com/articles?search=Multiple%20Servers)
- [Task Scheduling](https://www.msaied.com/articles?search=Task%20Scheduling)

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

  What cache drivers work with Laravel's onOneServer() and alwaysOnOneServer()?Only shared cache stores work: Redis, Memcached, DynamoDB, or the database driver. The file and array drivers are local to each server, so every server would take its own lock and the task would still run on all of them.

   Why do unnamed closure tasks still run on every server even after calling alwaysOnOneServer()?The lock key is built from the task name. Closure tasks have no name unless you call name() on them. Rather than throwing an exception, alwaysOnOneServer() silently skips unnamed closures, so they continue to run on every server. Add a name() call to any closure task you want covered.

   How does hasBeenInterruptedSince() differ from the old schedule:interrupt behavior?Previously, schedule:interrupt set a flag that only the schedule:run loop checked, so background commands launched with runInBackground() were never notified. In Laravel 13.35, the interrupt stores a timestamp. A long-running command can call hasBeenInterruptedSince() between batches to detect a deploy-time interrupt and exit cleanly, without being killed mid-process.

   ![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/758/1d3bc21cbf8aabd81b1a721134f37007.png) Laravel · 4 min read### Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()

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

  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)
