Colas y trabajo programado
Qué corre en segundo plano, cómo mantener vivo el worker y qué corre cron cada día.
Esta guía cubre las dos cosas que pasan fuera de una petición web: los jobs que esperan en una cola a un worker, y las tareas que el programador corre con un reloj. La necesitas cuando un job no corre, cuando un despliegue no surtió efecto en segundo plano, o cuando agregas trabajo propio.
La conexión de la cola
config/queue.php lee QUEUE_CONNECTION, por omisión database: los jobs son filas de la tabla jobs, que las migraciones del kit crean, y los jobs fallidos van a failed_jobs (driver database-uuids). No hace falta ningún servicio extra. Las conexiones y sus variables:
| Conexión | Variables | Notas |
|---|---|---|
database |
DB_QUEUE_CONNECTION (por omisión: la base de datos principal), DB_QUEUE_TABLE (jobs), DB_QUEUE (default), DB_QUEUE_RETRY_AFTER (10) |
La opción por omisión. retry_after es de diez segundos: un job que corre más que eso sin terminar se entrega a otro worker como si hubiera fallado. Súbelo por encima de tu job más largo, y mantenlo por encima del --timeout del worker |
redis |
REDIS_QUEUE_CONNECTION (default), REDIS_QUEUE (default), REDIS_QUEUE_RETRY_AFTER (90) |
Más rápido y más ligero para la base de datos. Necesita la conexión Redis de config/database.php |
sqs |
SQS_PREFIX, SQS_QUEUE, SQS_SUFFIX, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION |
Amazon SQS |
beanstalkd |
BEANSTALKD_QUEUE_HOST, BEANSTALKD_QUEUE, BEANSTALKD_QUEUE_RETRY_AFTER |
Beanstalkd |
sync |
ninguna | Corre el job dentro de la petición que lo despachó. Está bien para un script de prueba; en producción bloquea a la persona que provocó el trabajo |
QUEUE_FAILED_DRIVER (por omisión database-uuids) elige dónde se guardan los jobs fallidos. Los lotes de jobs usan la tabla job_batches.
Lo que el kit encola
| Job o clase | Se despacha cuando | Qué hace el worker |
|---|---|---|
App\Jobs\SendAnnouncementEmailsJob |
App\Observers\AnnouncementObserver ve un aviso guardado con estado Publicado, al crearse o al cambiar a ese estado |
Recorre a cada usuario no bloqueado, de cien en cien, y envía AnnouncementNotification a cada uno: correo cuando el aviso tiene "enviar correo" y el usuario acepta correo, web push cuando el usuario tiene un navegador suscrito. La entrega del aviso, peticiones push incluidas, ocurre aquí y no en la petición del administrador |
WeblaborMx\BillingCore\Jobs\MigrateUsersToPlan |
Un administrador confirma el modal "Migrar usuarios del plan", abierto desde "Migrar suscriptores" en un precio; se despacha después de que la transacción hace commit | Mueve las suscripciones seleccionadas, o todas, al precio destino a través del servicio de migración de planes |
Cualquier cosa que escribas que implements ShouldQueue |
Llamas a dispatch() o notify() |
Las notificaciones y mailables que implementan ShouldQueue se serializan y las envía el worker |
Lo que no se encola, para que sepas dónde buscar cuando algo va lento: las notificaciones construidas sobre App\Notifications\Notification se envían durante la petición, fila de base de datos, correo y push incluidos, porque la clase base usa Queueable sin ShouldQueue. El código de verificación de correo se envía de forma sincrónica con Mail::mailer(...)->send(), y el código por SMS también sale de forma sincrónica. Agrega implements ShouldQueue a una notificación tuya cuando se reparte a muchas personas; el job de avisos es el patrón para hacerlo en masa.
Para crear un job en cola usa el stub del proyecto, que ya implementa ShouldQueue:
php artisan make:job Orders/ArchiveOldOrders
Corre el worker en local
composer dev arranca cuatro procesos en una terminal: php artisan serve, php artisan queue:listen --tries=1, php artisan pail --timeout=0 (la cola del log) y npm run dev. Un job que despaches corre un momento después y su salida aparece en la misma terminal. queue:listen recarga el código en cada job, así que no lo reinicias después de editar una clase de job; por eso se usa aquí y no en producción.
Si prefieres no correr un worker, pon QUEUE_CONNECTION=sync en tu .env local. Todo corre en línea y las excepciones aparecen en la petición que las causó.
Corre el worker en producción
El worker es un proceso de larga vida:
php artisan queue:work --sleep=3 --tries=3 --max-time=3600
queue:work carga la aplicación una vez y la mantiene en memoria, que es lo que lo hace rápido y lo que lo deja viejo después de un despliegue. Mantenlo vivo con Supervisor; el bloque de programa completo está en /help/deploy-your-project. --sleep=3 es la pausa cuando la cola está vacía, --tries=3 los intentos antes de marcar un job como fallido, y --max-time=3600 la vida tras la cual el worker sale para que Supervisor lo reinicie.
Agrega --queue=high,default para atender varias colas con nombre en orden de prioridad, y --timeout=60 para matar un job que corra más de un minuto (mantenlo por debajo del retry_after de la conexión).
Reinícialo en cada despliegue
php artisan queue:restart
El comando escribe una marca de tiempo en el caché; cada worker la revisa después de cada job y sale cuando es más nueva que su propio arranque. Supervisor entonces arranca un worker nuevo, que carga el código nuevo. Dos consecuencias: el almacén de caché tiene que ser uno que compartan el worker y el proceso web (database o redis, nunca array), y un job que está corriendo cuando lo llamas termina primero. Sin este comando el worker sigue ejecutando la versión anterior: una notificación cuyo texto cambiaste sigue llegando con el texto viejo hasta que el worker muera por casualidad.
Jobs fallidos
Un job que lanza una excepción en cada uno de sus --tries intentos cae en failed_jobs con su excepción. La excepción también se reporta por los canales normales: el log, y Slack cuando SLACK_BOT_TOKEN está definido.
php artisan queue:failed # listarlos, con id, conexión, cola y fecha
php artisan queue:retry 9f1c-... # devolver uno a la cola por su id
php artisan queue:retry all # devolverlos todos
php artisan queue:forget 9f1c-... # borrar uno sin reintentar
php artisan queue:flush # borrarlos todos
php artisan queue:prune-failed --hours=168 # borrar los de más de una semana
Reintenta después de corregir la causa y reiniciar el worker; reintentar un job sobre el código viejo lo vuelve a fallar.
Tareas programadas
El programador corre todo lo registrado con Schedule:: en routes/console.php y en los service providers de los paquetes. Dos tareas vienen con el kit:
| Comando | Cuándo | Qué hace |
|---|---|---|
php artisan stats:compute-daily |
Cada día a las 00:00 | Cuenta los usuarios cuyo last_logged_at es ayer y guarda el número en la tabla stats bajo la clave daily_logged_users, una fila por día. El tablero del panel de administración lee esa tabla. Declarado en routes/console.php |
php artisan billing:sync-exchange-rates |
Cada día a las 12:00, sólo cuando FEATURE_PLANS_ENABLED o FEATURE_ADDONS_ENABLED está encendida |
Busca las fechas de pago de suscripciones pagadas cobradas en una moneda distinta de primary_currency en config/pricing.php (por omisión mxn), y obtiene y guarda el tipo de cambio de cada fecha que no lo tenga. El proveedor es Frankfurter (exchange_rates.endpoint, https://api.frankfurter.app, tiempo límite exchange_rates.timeout, diez segundos). --date=2026-09-01 sincroniza una sola fecha. Declarado en el service provider de Billing Core; la configuración de monedas está en /help/currencies-intervals-and-exchange-rates |
Las horas están en la zona horaria de la aplicación, UTC en config/app.php. php artisan schedule:list imprime la tabla con la próxima ejecución de cada una.
Nada corre a menos que cron llame al programador cada minuto:
* * * * * cd /var/www/your-project && php artisan schedule:run >> /dev/null 2>&1
schedule:run mira el reloj, corre lo que toque y sale. En local, php artisan schedule:work hace lo mismo en primer plano una vez por minuto, y php artisan schedule:test te deja elegir una tarea y correrla ahora.
Agrega las tuyas
Un job va en app/Jobs, un comando en app/Console/Commands (se registra solo), y su programación en routes/console.php:
Schedule::command('orders:archive-old')->dailyAt('03:00');
Schedule::job(new ArchiveOldOrders)->weeklyOn(1, '04:00');
Un comando programado que tarda, o que no debe traslaparse consigo mismo, lleva ->withoutOverlapping(); uno que deba correr en un solo servidor cuando tienes varios lleva ->onOneServer(), que necesita un almacén de caché compartido.