Desarrollo Web 5-8 minutos

Tareas programadas en Laravel 13: adiós al crontab, hola al scheduler

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Compartir:
Tareas programadas en Laravel 13: adiós al crontab, hola al scheduler

Una sola línea de cron ejecuta php artisan schedule:run cada minuto, y el horario completo de tus tareas programadas vive en PHP dentro de Laravel 13: sin crontabs dispersos, sin tareas huérfanas y con todo bajo control de versiones. El task scheduler convierte el reloj del servidor en código que puedes probar y desplegar.

Por qué el scheduler de Laravel mejora el crontab

Un servidor con diez líneas de crontab que nadie se atreve a tocar es más común de lo que parece. Cada tarea vive fuera de la aplicación, sin control de versiones, sin logs centralizados y sin forma de probarla antes de que falle en producción. El scheduler de Laravel 13 resuelve ese problema de raíz: defines todo el horario en PHP y el servidor solo necesita una entrada cron.

Una sola entrada cron frente a decenas de líneas

Con crontab, cada tarea es una línea con su propia sintaxis, su propio entorno y su propia ruta al binario de PHP. Con el scheduler, todo eso se reduce a una única entrada que se repite en cualquier servidor:

* * * * * cd /ruta/al/proyecto && php artisan schedule:run >> /dev/null 2>&1

Cada minuto, Laravel evalúa el horario definido en la aplicación y ejecuta únicamente las tareas que tocan en ese momento. Añadir una tarea nueva ya no significa editar el crontab: es un cambio de código como cualquier otro.

El horario vive en el código: versiones, tests y despliegues

Al estar definido en PHP, el horario viaja con el repositorio: puedes revisarlo en un pull request, ejecutarlo en local y desplegarlo con el mismo flujo que el resto de la aplicación. Si una tarea deja de hacer falta, se elimina del código y desaparece de todos los entornos a la vez, sin dejar líneas huérfanas en servidores olvidados.

Dónde se define el horario en Laravel 13: routes/console.php

Desde Laravel 11, el horario se define en routes/console.php usando la fachada Schedule. Es un fichero familiar para cualquier desarrollador Laravel, con la misma filosofía que las rutas HTTP: declarativo, legible y agrupado.

Schedule::command, Schedule::job y Schedule::call

El scheduler acepta tres tipos de tareas: comandos Artisan, jobs de cola y closures.

use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->daily();
Schedule::job(new GenerarSitemap)->weekly();
Schedule::call(function () {
    Cache::forget('stats_home');
})->hourly();

Los comandos son la opción más común porque reutilizan toda la lógica de Artisan, con sus argumentos, opciones y tests. Los jobs encolan trabajo para que lo procesen los workers de cola, y los closures sirven para tareas pequeñas que no merecen un comando propio.

schedule:list: ver el plan completo de un vistazo

Antes de confiar en un horario, conviene inspeccionarlo. El comando schedule:list muestra todas las tareas registradas con su frecuencia, su próxima ejecución y las condiciones asociadas, una tabla que suele destapar tareas duplicadas o frecuencias que no eran las que creías.

Frecuencias: de cada minuto a expresiones cron

everyMinute, hourly, dailyAt y compañía

La API de frecuencias es expresiva y cubre casi cualquier necesidad sin escribir una expresión cron: everyMinute(), everyFiveMinutes(), hourly(), daily(), dailyAt('03:00'), weekly(), monthly() y decenas de variantes. Cuando necesitas algo más específico, cron('*/15 * * * *') acepta la expresión clásica.

Días concretos con days() y zonas horarias con timezone()

Para tareas que solo tocan ciertos días, el encadenado days() filtra el día de la semana o fechas concretas, y timezone() fija la zona horaria de la tarea independientemente de la del servidor:

Schedule::command('reportes:semanal')
    ->weekly()
    ->days([1, 3, 5])
    ->timezone('Europe/Madrid');

Condiciones: entre horas, días laborables y when()

Las frecuencias se combinan con condiciones para afinar cuándo se ejecuta una tarea. between('8:00', '20:00') limita la ventana horaria, weekdays() y weekends() seleccionan el tipo de día, y when() acepta un closure que devuelve true o false: si la condición falla, la tarea se salta esa ejecución.

Schedule::command('backups:run')
    ->dailyAt('02:00')
    ->when(fn () => disk('backups')->freeSpace() > 10 * 1024 * 1024);

Evitar solapamientos y duplicados

withoutOverlapping(): el lock en caché

Una tarea que tarda más de lo previsto puede arrancar de nuevo antes de terminar, duplicando trabajo o corrompiendo datos. withoutOverlapping() evita que una tarea se lance si la anterior sigue en ejecución, usando un lock en el caché que por defecto es el driver file y puede cambiarse a Redis en aplicaciones con varios procesos:

Schedule::command('importar:productos')
    ->hourly()
    ->withoutOverlapping();

onOneServer() cuando la app corre en varios servidores

Si la aplicación se despliega en varios servidores, el cron de cada uno ejecutará el scheduler y las tareas se lanzarán varias veces. onOneServer() limita la ejecución a un único servidor, siempre que el caché sea compartido (Redis o base de datos), de modo que la tarea corre una sola vez en todo el clúster.

runInBackground() para tareas largas

El scheduler ejecuta las tareas de forma secuencial: si una tarea lenta bloquea el ciclo de un minuto, las demás se retrasan. runInBackground() lanza la tarea en segundo plano y deja el scheduler libre para seguir evaluando el resto del plan.

Salida y notificaciones: logs, ficheros y email

sendOutputTo y appendOutputTo

Por defecto, la salida de un comando programado se pierde. sendOutputTo() la redirige a un fichero, sobrescribiéndolo en cada ejecución, y appendOutputTo() añade al final del fichero para conservar un historial:

Schedule::command('limpieza:tokens')
    ->daily()
    ->appendOutputTo(storage_path('logs/limpieza.log'));

emailOutputTo y los hooks onSuccess y onFailure

Para saber qué ha pasado sin mirar logs, emailOutputTo() envía la salida por correo, y los hooks onSuccess() y onFailure() permiten reaccionar al resultado: notificar a un canal de Slack, marcar una incidencia o simplemente registrar que la tarea terminó bien.

Schedule::command('resumen:mensual')
    ->monthly()
    ->emailOutputTo('admin@blenderdeluxe.com')
    ->onFailure(fn () => Log::error('El resumen mensual falló'));

Desarrollo y pruebas: schedule:work y schedule:test

En local no quieres un cron apuntando a tu máquina. schedule:work mantiene el scheduler en primer plano y ejecuta las tareas según su frecuencia, ideal para comprobar que el plan funciona. Para validar cada tarea sin esperar a que llegue su hora, schedule:test recorre el plan y pregunta si quieres ejecutar cada una, una a una. Combinado con schedule:list, tienes inspección, simulación y ejecución real sin tocar el crontab del sistema.

Ejemplo completo: el mantenimiento de un blog en Laravel 13

Un blog como este necesita poco mantenimiento, pero lo que necesita debe ocurrir sí o sí. Con el scheduler, todo el plan cabe en routes/console.php y se despliega con el resto del código.

Limpieza diaria de tokens y sesiones caducadas

Schedule::command('auth:clear-resets')->daily();
Schedule::call(function () {
    DB::table('sessions')
        ->where('last_activity', '<', now()->subDays(30)->getTimestamp())
        ->delete();
})->dailyAt('04:00')->withoutOverlapping();

Sitemap semanal y resumen mensual por email

Schedule::command('sitemap:generate')
    ->weekly()
    ->sundays()
    ->at('06:00');

Schedule::command('stats:monthly-digest')
    ->monthlyOn(1, '08:00')
    ->emailOutputTo('admin@blenderdeluxe.com');

El antes era un crontab con líneas sueltas, cada una con su propia sintaxis y sin logs. El después es un fichero versionado que se prueba con schedule:test y se despliega con un push. Cuando la tarea falla, el hook onFailure lo cuenta; cuando tarda más de la cuenta, withoutOverlapping evita la doble ejecución.

Conclusión

El task scheduler de Laravel 13 convierte el crontab disperso en una sola línea de cron y un fichero PHP versionado, con frecuencias expresivas, condiciones, locks contra solapamientos y notificaciones. Las colas deciden cuándo hay un worker libre para trabajar; el scheduler es el reloj que decide cuándo toca cada tarea, y ambos se combinan: el scheduler encola jobs y Horizon los procesa. Si gestionas servidores con tareas repetitivas, este es el primer paso para que el servidor se mantenga solo. Sigue leyendo el blog para más guías de Laravel 13, desde colas y notificaciones hasta el despliegue con Sail.

Categorías