Solución de problemas · Laravel
¿El scheduler de Laravel no se ejecuta, o buscas una alternativa?
Por qué el scheduler de Laravel para silenciosamente — la entrada cron ausente, usuario incorrecto, entorno y tareas solapadas — y cómo solucionarlo, o sustituirlo.
El scheduler de Laravel funciona gracias a una única entrada cron del sistema. Si esa entrada es incorrecta — o el entorno bajo ella lo es — todas las tareas programadas dejan de funcionar silenciosamente. Aquí está la lista de comprobación.
1. La entrada cron debe existir
Laravel necesita exactamente una línea en crontab que llame a schedule:run cada
minuto. Confirma que está para el usuario correcto (normalmente tu usuario de
despliegue, no root):
crontab -l
Debe verse así:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
Si falta, añádela con crontab -e. Un error habitual en el despliegue es añadirla
como root mientras la app corre como www-data (o al revés).
2. Usa el binario PHP correcto y la ruta absoluta
Bajo cron, php puede no resolverse, o puede ser la versión incorrecta. Usa la
ruta completa y la ruta del proyecto:
* * * * * cd /var/www/app && /usr/bin/php8.3 artisan schedule:run >> /dev/null 2>&1
3. El entorno es diferente al de tu shell
Cron no carga tu perfil de shell, así que todo lo que hayas definido allí no
estará disponible. Laravel lee .env, lo cual está bien — pero si tus tareas
llaman a otros binarios, dales un PATH explícito. Asegúrate también de que
APP_ENV y la configuración de cola y caché coincidan con producción.
4. Silenciaste la salida y ahora no puedes depurar
>> /dev/null 2>&1 oculta todo, incluidos los errores. Regístralo temporalmente:
* * * * * cd /var/www/app && php artisan schedule:run >> storage/logs/schedule.log 2>&1
Luego ejecuta php artisan schedule:run a mano y lee la salida — la mayoría de
los fallos (permisos, variables de entorno ausentes, conexión a BD) aparecen
de inmediato.
5. Las tareas se solapan o se cuelgan
Una tarea larga que se ejecuta cada minuto puede acumularse. Usa los guards de Laravel para que una ejecución lenta no bloquee la siguiente:
$schedule->command('reports:build')
->hourly()
->withoutOverlapping()
->onOneServer();
El problema más profundo: el scheduler puede parar y quedarse callado
Si schedule:run deja de dispararse — el servidor se reinició, el crontab se
borró en el despliegue, se actualizó PHP — Laravel no tiene forma de avisarte.
Tus informes y correos en cola simplemente se detienen.
Haz ping a un heartbeat desde una tarea programada para saber que el scheduler en sí está vivo:
$schedule->call(function () {
Http::timeout(10)->get('https://ping.steadycron.com/<tu-ping-token>');
})->everyFifteenMinutes();
Si ese ping desaparece, SteadyCron te alerta — el scheduler está caído, antes de que tus usuarios lo noten.
¿Buscas una alternativa al Scheduler de Laravel directamente?
Si estás cansado de arreglar el mismo tipo de problema una y otra vez — un
crontab borrado, una migración de servidor que elimina la única línea de la
que depende todo, un schedule:run que técnicamente se dispara pero falla en
silencio — no tienes por qué mantener ese punto único de fallo.
La mayoría de entradas $schedule->command(...) y $schedule->call(...)
pueden convertirse en una ruta:
// routes/api.php
Route::post('/cron/build-reports', function () {
Artisan::call('reports:build');
return response()->json(['ok' => true]);
})->middleware('auth.cron-token');
SteadyCron llama entonces a ese endpoint según el horario — con su propio
retry/backoff, timeout y registro de ejecuciones — en lugar de depender de
que exista una entrada cron del sistema, del binario PHP correcto, o de que
schedule:run se invoque siquiera. El horario ahora vive en SteadyCron,
sobrevive intacto a las migraciones de servidor, y cada ejecución muestra su
código de estado y respuesta, no solo lo que llegó a storage/logs.
Es la misma idea que se trata con más detalle en alternativa al cron de Docker — sacar el disparador del proceso que debería limitarse a servir peticiones.