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() so long-running commands can stop cleanly during a deploy.

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

ShareCopy linkCopied

 ![Running Laravel's Scheduler on Multiple Servers with alwaysOnOneServer()](https://cdn.msaied.com/773/71b059a70544e682293d8b26b2566611.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. That is fine for clearing 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 13.35 ships two additions that make multi-server scheduling much easier to manage.

---

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

Laravel has long had `onOneServer()`, which takes a distributed cache lock so only the first server to start a task runs it:

```php
Schedule::command('reports:send')->daily()->onOneServer();

```

The catch is that you have to remember to add it to every task. Miss one and duplicates only surface in production. A group workaround existed, but Laravel 13.35 adds a cleaner global flag contributed by [Jack Bayliss](https://github.com/jackbayliss):

```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 `Schedule::alwaysOnOneServer(false)` turns it back off.

### Name Your Closure Tasks

The lock key is derived from the task's name. Artisan commands have one automatically; closure tasks do not unless you call `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 keyed
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 closure tasks for a `name()` call before enabling the flag.

### Shared Cache Is Required

The lock lives 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 app uses `file` for its own caching but you need a shared store for the scheduler, use `Schedule::useCache()`:

```php
public function boot(): void
{
    Schedule::alwaysOnOneServer();
    Schedule::useCache('redis'); // named store from config/cache.php
}

```

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

---

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

Before 13.35, `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 could finish on stale code or be 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. A long-running command can now poll that time 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...
php artisan queue:restart

```

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

---

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

- `Schedule::alwaysOnOneServer()` in `AppServiceProvider::boot()` applies `onOneServer()` globally — no more per-task calls.
- Closure tasks must have a `name()` call to participate; unnamed closures are silently skipped.
- All servers must share the same cache store; use `Schedule::useCache('redis')` if your default store is local.
- `Schedule::hasBeenInterruptedSince($startedAt)` lets background commands stop cleanly between batches during a deploy.
- The interrupt timestamp approach mirrors `queue:restart`, making deploy scripts consistent.
- 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://msaied.com/articles?search=Laravel)
- [Scheduler](https://msaied.com/articles?search=Scheduler)
- [Laravel 13.35](https://msaied.com/articles?search=Laravel%2013.35)
- [Multi-Server](https://msaied.com/articles?search=Multi-Server)
- [Task Scheduling](https://msaied.com/articles?search=Task%20Scheduling)

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

  What happens to unnamed closure tasks when alwaysOnOneServer() is enabled?Unnamed closure tasks are silently skipped by alwaysOnOneServer() because the lock key is derived from the task name. They continue to run on every server. Add a name() call to a closure task to make it respect the flag.

   Does Schedule::useCache() affect my entire application's cache configuration?No. useCache() only changes the store used for scheduler locks, specifically onOneServer() and withoutOverlapping(). The rest of your application continues to use its default cache store.

   Will hasBeenInterruptedSince() trigger on an interrupt from a previous deploy?No. hasBeenInterruptedSince() only returns true if the interrupt occurred at or after the timestamp you pass in (typically now() captured at the start of the command), 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: 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/772/751d21c9bbf4187742acfb130ad4b3c4.png) Laravel · 3 min read### Puzzle Party: A Laravel + Flutter App Built With the Help of the Community

12 Oct 2026 ](https://msaied.com/articles/puzzle-party-a-laravel-flutter-app-built-with-the-help-of-the-community) [ ![](https://cdn.msaied.com/771/c168cfa614dbed6663e8550e8d20a489.png) Composer Pacakge · 4 min read### BashBox: A Sandboxed Bash Interpreter for AI Agents in PHP

11 Oct 2026 ](https://msaied.com/articles/bashbox-a-sandboxed-bash-interpreter-for-ai-agents-in-php-1) [ ![](https://cdn.msaied.com/770/9ffc1bfe88806626f495f3ef307e0b69.png) Composer Pacakge · 4 min read### BashBox: A Sandboxed Bash Interpreter for AI Agents in PHP

11 Oct 2026 ](https://msaied.com/articles/bashbox-a-sandboxed-bash-interpreter-for-ai-agents-in-php) 

  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)
