Back to blog
LaravelDockerHomelab

Laravel 13.35 makes scheduler locking easier

A global setting to avoid duplicate scheduled tasks when Laravel runs across multiple replicas.

By Isma3 min read

Replicating the scheduler creates a real problem

Running several Docker replicas of a Laravel application is common, including in a homelab. The trouble starts when every replica runs php artisan schedule:run: the same scheduled task may start in every container at once. That is not harmless for jobs that send notifications, build reports, or synchronize external data.

Until now, the usual approach was adding onOneServer() to every schedule definition that needed it. It works, but it relies on remembering the method whenever a new task is added. As an application grows, that repeated manual step becomes an easy place to make a mistake.

One setting for the whole schedule

Laravel 13.35 adds Schedule::alwaysOnOneServer(). It applies the behavior of onOneServer() across the application's scheduled tasks, without chaining it onto each definition individually. The code change is small, but it is useful when the scheduler runs on multiple hosts or replicas of the same service.

Laravel documents this call in the boot method of AppServiceProvider. The locking mechanism depends on a shared cache backend that supports atomic locks, so every node needs access to the same backend. The supported cache drivers listed in the documentation are database, memcached, dynamodb, and redis.

<?php
 
namespace App\Providers;
 
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Schedule::alwaysOnOneServer();
    }
}

For a Docker deployment, this means the scheduler does not have to become a single-replica service just to avoid duplicate work. You can keep multiple replicas available and let Laravel determine which one obtains the lock for each run. It also removes one thing to remember whenever a new scheduled task is introduced.

The details that still matter

This will not work with an isolated local cache inside every container. If each replica has its own cache storage, each one will see its own lock and the tasks will still run multiple times. The cache must be central and reachable by every process that runs the scheduler.

alwaysOnOneServer() is not a replacement for withoutOverlapping(). The first decides which server runs a scheduled task, while the second prevents a new run when the previous one is still active. If a task can take longer than its configured interval, both concerns should be considered separately.

There is also one exception worth checking before enabling the global setting: scheduled closures without name() are excluded and will still run on every server. If you use Schedule::call() in your schedule, give it a stable name when it must use distributed locking. Names should also distinguish similar tasks that run with different parameters.

This is not a replacement for deployment testing. Bring up at least two replicas against the same cache backend and verify the behavior of critical tasks, especially those involving payments, email, or external integrations. php artisan schedule:list remains useful for reviewing the resulting schedule before assuming everything is covered.


Source: Laravel Framework v13.35.0

Official documentation: running tasks on one server

Newsletter

What I learn coding with AI, every two weeks in your inbox.

Real numbers, mistakes included. No spam, unsubscribe in one click.

No spam, unsubscribe in one click. I only use your email to send you this and the newsletter. More in the privacy policy.