/articoli
6 min di lettura

Laravel queues: worker, retry e pausa

Laravel queues nella 13: attributi sui job, Queue::route, quale driver scegliere, worker in Docker senza Supervisor, retry e pausa di una singola coda.


Una coda in Laravel sposta il lavoro lento fuori dal ciclo della richiesta: il controller fa dispatch(), la risposta HTTP parte subito e un processo separato esegue il job in background. Su Laravel 13 la configurazione del singolo job si dichiara con gli attributi PHP al posto delle proprietà di classe, la scelta della coda si centralizza con Queue::route() e una coda si può mettere in pausa da riga di comando senza spegnere i worker. Restano da decidere due cose: chi tiene vivo il processo che consuma i job, e cosa succede a quelli che falliscono.

Cosa cambia nei job con Laravel 13

Rispetto alle versioni precedenti la politica di retry si legge dagli attributi sopra la classe, invece che dalle proprietà pubbliche sparse nel corpo del job. Su una installazione laravel/framework v13.12.0 la cartella degli attributi di coda ne contiene dodici, tra cui Tries, Backoff, Timeout, FailOnTimeout, MaxExceptions, Queue, Connection, Delay, UniqueFor e DebounceFor.

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;

#[Tries(5)]
#[Backoff(5, 30, 90)]
#[Timeout(120)]
#[FailOnTimeout]
class GeneraFatturaMensile implements ShouldQueue
{
    public function handle(): void
    {
        // ...
    }
}

Chi apre quel file vede subito che il job ritenta cinque volte, aspetta cinque, trenta e novanta secondi tra un tentativo e l'altro, e viene marcato come fallito se supera i due minuti. Le vecchie proprietà $tries e $backoff funzionano ancora, quindi sul codice esistente non c'è nessuna riscrittura obbligata.

Queue::route() porta invece la scelta di coda e connessione fuori sia dalle classi dei job sia dai punti di dispatch, dentro un service provider:

Queue::route(GeneraFatturaMensile::class, queue: 'fatture', connection: 'redis');

C'è poi #[DebounceFor(30)], che scarta i dispatch ripetuti dello stesso job dentro una finestra di tempo ed esegue solo l'ultimo, con un maxWait opzionale per garantire che prima o poi parta comunque. Risolve il caso del modello salvato dieci volte di fila quando la reindicizzazione ti serve una volta sola.

Quale driver di coda conviene scegliere

Il file config/queue.php di una installazione nuova arriva con otto connessioni già scritte: sync, database, beanstalkd, sqs, redis, deferred, background e failover, ma nella pratica finirai per usarne tre.

Il driver database regge finché i job al minuto si contano sulle dita e hai già Postgres o MySQL in piedi, perché non ti obbliga ad aggiungere un servizio da mantenere. Redis diventa il default sensato appena il volume sale, ed è anche l'unico su cui gira Horizon. La connessione sqs ha senso quando sei già dentro AWS e vuoi che la coda sopravviva alla morte della macchina applicativa.

Il failover merita una menzione perché quasi nessuno lo conosce. Gli passi una lista ordinata di connessioni e, se la prima non risponde, il job finisce nella successiva invece di esplodere in faccia all'utente.

Come si tiene vivo un worker in produzione

Il worker è un processo di lunga durata che qualcun altro deve riavviare quando muore, e la documentazione ufficiale propone Supervisor per quel compito. Su un server tradizionale funziona bene. Se però l'applicazione gira già in container quel pezzo diventa ridondante, perché Docker Compose fa già da process manager e infilare Supervisor dentro un container significa sovrapporre due supervisori.

Su questo blog il worker è un container separato che parte dalla stessa immagine dell'app, con una variabile d'ambiente che ne decide il ruolo, e Compose lo tiene su con restart: unless-stopped. La struttura completa dei sette container la trovi nel post sul deploy di Laravel su VPS con Docker Compose, che gira sopra a FrankenPHP in produzione. Scalare vuol dire alzare il numero di repliche del servizio, senza toccare una riga dell'applicazione.

queue:work è un demone e tiene in memoria il codice caricato all'avvio, quindi dopo un rilascio continua a eseguire la versione vecchia finché non gli dici di uscire. Un php artisan queue:restart fa terminare i worker in modo pulito appena chiuso il job corrente, e il process manager li rimette su aggiornati. Quella riga va in fondo allo script di deploy, sempre.

Esiste ancora queue:listen, che ricarica il framework a ogni singolo job ed è comodo mentre sviluppi, ma in produzione pagheresti il boot completo di Laravel per ogni job consumato.

Cosa fare quando un job fallisce

Un job che esaurisce i tentativi finisce nella tabella dei falliti, e il driver predefinito è database-uuids. Da lì lavori con tre comandi: queue:failed elenca quello che è caduto, queue:retry rimette in coda un identificativo specifico oppure l'intera tabella con queue:retry all, e queue:forget cancella una riga quando è chiaro che non ha senso ritentarla.

La tabella va potata, altrimenti cresce per sempre e diventa illeggibile. Con queue:prune-failed --hours=168 tieni l'ultima settimana e butti il resto, e per i batch c'è queue:prune-batches.

Per accorgersene prima di un utente c'è queue:monitor, che prende i nomi delle code da sorvegliare, accetta --max=1000 come soglia oltre la quale emette un evento e ha un flag --json comodo da collegare a un sistema di allerta esterno. Non sostituisce Horizon, ma su un'installazione senza Redis è l'unica cosa che hai.

Come si mette in pausa una coda senza fermare i worker

Dalla versione 12.40 di Laravel esistono queue:pause e queue:resume, che prendono il nome di una coda e ne sospendono o riprendono il consumo lasciando acceso il processo worker. Rispetto a spegnere il container, i job continuano ad accumularsi e riprendono da dove erano rimasti, senza perdere niente e senza toccare le altre code.

Serve durante gli incidenti, quando un servizio esterno è giù e ogni job che parte brucia un tentativo per niente. Metti in pausa quella singola coda, aspetti che il fornitore torni su, poi fai queue:resume. Tutte le altre nel frattempo continuano a girare.

Da dove partire se le code non le hai mai usate

Il percorso più corto è spostare in coda una sola cosa lenta e già isolata, tipicamente l'invio di una email o la generazione di un PDF, tenendo il driver database per non aggiungere servizi. Fai girare il worker in locale, guarda cosa succede quando il job va in errore di proposito, e solo dopo passa a Redis e a un process manager vero. Una spinta tipica arriva da Laravel Scout, che manda l'indicizzazione in background e obbliga a capire come funzionano i job, come nel post su Scout e i post schedulati.

Domande frequenti

Le code Laravel funzionano senza Redis? Sì, il driver database usa una tabella normale e non richiede nessun servizio aggiuntivo. Redis conviene quando il numero di job cresce, perché evita di caricare di scritture il database applicativo.

Ogni quanto va riavviato un worker? A ogni deploy con queue:restart, altrimenti resta sul codice vecchio. Aggiungere --max-jobs o --max-time a queue:work lo fa uscire da solo dopo un tot di lavoro, cosa che limita i danni di eventuali perdite di memoria.

Quanti worker servono per le code Laravel? Si parte da uno e si guarda la profondità della coda con queue:monitor. Se cresce più in fretta di quanto viene smaltita, aggiungi repliche finché la lunghezza media resta stabile.

Horizon serve per forza? No, e funziona solo con Redis. Dà una dashboard con throughput, tempi di attesa e job falliti, quindi diventa utile quando le code sono più di due.


Grazie per aver letto fin qui.

Se l'articolo ti è stato utile, iscriviti alla newsletter per ricevere il prossimo direttamente in casella, oppure aggiungi il feed RSS al tuo lettore.

// similarity

// iscriviti

Ricevi il prossimo articolo, via email.