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\MigrateSubscriptionsToPlan |
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 |
TicketOpenedNotification, TicketRepliedNotification, TicketAnsweredNotification |
Se abre un ticket, un usuario lo responde o lo reabre, o el equipo lo contesta | Envía el aviso a cada destinatario: centro de notificaciones, correo y push, con el evento en tiempo real y el registro de comunicaciones. Quien actuó recibe la respuesta de inmediato |
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; los tres avisos de tickets de arriba son la excepción. 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. Entre las tareas que 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 |
php artisan billing:return-to-free-plan |
Cada día a las 03:30, sólo cuando FEATURE_PLANS_ENABLED está encendida |
Devuelve al plan gratuito de su país a cada cuenta cuya suscripción terminó y que no tiene plan, como ya lo hace por sí solo el fin de la suscripción; una cuenta que no puede mover conserva el motivo y se vuelve a intentar al día siguiente. Declarado en el service provider de Billing Core; ver /help/when-your-account-is-left-without-a-plan |
php artisan media:purge-unrelated |
Cada hora | Borra los archivos subidos hace más de 24 horas que nunca se guardaron en un registro, como una subida que quedó atrás al abandonar un formulario, y libera su espacio. Declarado en routes/console.php |
php artisan tickets:close-unattended |
Cada hora, sólo cuando FEATURE_TICKETS_ENABLED está encendida |
Cierra los tickets contestados a los que nadie respondió después de FEATURE_TICKETS_AUTO_CLOSE_DAYS, y avisa a sus dueños en cuanto pasan FEATURE_TICKETS_AUTO_CLOSE_WARNING_DAYS. Declarado en routes/console.php; ver /help/support-tickets |
php artisan tickets:due-summary |
De lunes a viernes a las 08:00, sólo cuando FEATURE_TICKETS_ENABLED está encendida |
Envía al equipo de soporte un resumen de los tickets cerca de su plazo o vencidos, cuando hay al menos uno. Declarado en routes/console.php |
php artisan support-chats:sweep |
Cada minuto, sólo mientras el chat en vivo está encendido | Cierra los chats inactivos por FEATURE_TICKETS_CHAT_IDLE_MINUTES y mantiene la fila en movimiento. Las pantallas de soporte abiertas corren el mismo barrido, así que sin el programador un chat solo se cierra mientras alguien tiene una abierta. Declarado en routes/console.php; ver /help/support-chat |
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.