Laravel 13.35 simplifica el bloqueo del scheduler
Un ajuste global para evitar tareas duplicadas cuando ejecutas el scheduler de Laravel en varias réplicas.
El problema aparece al duplicar el scheduler
Tener varias réplicas Docker de una aplicación Laravel es habitual, también en un homelab. El problema llega cuando todas ejecutan php artisan schedule:run: una tarea programada puede arrancar en cada contenedor al mismo tiempo. Para procesos que envían avisos, generan informes o sincronizan datos, esa duplicación no es un detalle menor.
Hasta ahora, la forma habitual de evitarlo era añadir onOneServer() en cada definición del calendario que lo necesitara. Funciona, pero depende de no olvidarse del método al crear una tarea nueva. En una aplicación que crece, esa repetición acaba siendo una fuente razonable de errores.
Un ajuste global para las tareas programadas
Laravel 13.35 incorpora Schedule::alwaysOnOneServer(). La llamada aplica el comportamiento de onOneServer() a las tareas programadas de la aplicación sin tener que encadenarlo una por una. Es una mejora pequeña en código, pero útil cuando el scheduler se ejecuta desde varias máquinas o réplicas del mismo servicio.
La documentación indica que debe declararse en el método boot de AppServiceProvider. El bloqueo se apoya en una caché compartida que permita bloqueos atómicos, así que todos los nodos deben comunicarse con el mismo backend. Laravel documenta como opciones compatibles los drivers database, memcached, dynamodb y redis.
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Schedule::alwaysOnOneServer();
}
}En un despliegue con Docker, esto evita convertir el scheduler en un servicio especial de una sola réplica solo para prevenir ejecuciones duplicadas. Puedes mantener varias réplicas disponibles y dejar que Laravel decida cuál obtiene el bloqueo para cada ejecución. También reduce la necesidad de revisar cada tarea nueva para comprobar si tiene onOneServer().
La letra pequeña importa
Esto no funciona con una caché local e independiente dentro de cada contenedor. Si cada réplica usa su propio almacenamiento de caché, cada una verá su propio bloqueo y las tareas seguirán ejecutándose varias veces. La caché tiene que ser central y accesible para todos los procesos que lanzan el scheduler.
alwaysOnOneServer() tampoco sustituye a withoutOverlapping(). El primero decide qué servidor ejecuta una tarea programada; el segundo evita una nueva ejecución si la anterior todavía sigue en marcha. Si una tarea puede tardar más que su frecuencia programada, conviene valorar ambos comportamientos por separado.
Hay además una excepción que conviene revisar antes de activar el cambio global: las closures programadas sin name() quedan fuera y continúan ejecutándose en todos los servidores. Si tienes Schedule::call() en tu calendario, asígnale un nombre estable cuando deba respetar el bloqueo distribuido. Para tareas similares con parámetros distintos, el nombre también debe distinguir cada definición.
No es un cambio que sustituya las pruebas de despliegue. Conviene levantar al menos dos réplicas contra la misma caché y comprobar qué ocurre con las tareas críticas, especialmente las que tocan pagos, correos o integraciones externas. php artisan schedule:list sigue siendo una buena comprobación para revisar el calendario resultante antes de dar por hecho que todo está cubierto.