Il tuo e-commerce impiega 8 secondi per mostrare il riepilogo dell'ordine? Il pannello di amministrazione si blocca mentre invii 200 email di notifica? È il classico sintomo di un problema che abbiamo visto decine di volte: stai eseguendo operazioni pesanti nel ciclo richiesta-risposta. Un sito non è una vetrina da ammirare: è uno strumento che deve vendere. Se il cliente aspetta, perdi. Noi, di Meteora Web, abbiamo costruito piattaforme che gestiscono migliaia di job al giorno senza far aspettare nessuno. In questa guida ti spieghiamo come funzionano le code di Laravel, come impostarle e, soprattutto, come gestire i fallimenti senza perdere dati.
Perché mettere in coda un lavoro
Immagina una cucina di ristorante. Il cameriere prende l'ordine (la richiesta HTTP) e lo passa in cucina (il job). Il cliente non aspetta che il piatto sia pronto: torna al tavolo, beve il suo vino, e intanto la cucina lavora in parallelo. Lo stesso vale per un'applicazione web. Un job in coda permette di:
Migliorare l'esperienza utente: la pagina risponde subito, anche se dietro le quinte sta girando un'elaborazione lunga (invio email, generazione PDF, elaborazione immagini).
Scalare orizzontalmente: puoi aggiungere più worker (lavoratori) per processare più job contemporaneamente, senza toccare il server web.
Gestire i picchi di carico: la coda si accumula e i worker svuotano il backlog, senza far crashare il sito.
Noi lo misuriamo in secondi di risposta: un job di invio email può passare da 3 secondi a 3 millisecondi per l'utente. Il resto viene dopo, in coda. E il cliente non se ne accorge.
Le connessioni: quale scegliere
Laravel supporta diversi driver per le code. La scelta dipende dal volume, dal budget e dall'infrastruttura.
Sponsored Protocol
Database
Il driver database usa una tabella MySQL/PostgreSQL. È il più semplice: nessun servizio esterno, solo un worker PHP. Ottimo per progetti piccoli o medi (fino a poche migliaia di job al giorno). Noi lo usiamo nei primi mesi di un progetto, quando il volume è basso e vogliamo tenere i costi a zero.
Svantaggio: scrive sul database a ogni ciclo, può diventare collo di bottiglia. Per carichi alti, meglio passare a Redis.
Redis
Driver performante: le code sono liste in memoria, velocissime. Con Horizon (il pannello di monitoring ufficiale per Redis) hai metriche, restart automatici, bilanciamento. Noi lo raccomandiamo per progetti in produzione con volumi medi/alti. Richiede Redis installato e configurato.
Amazon SQS
Soluzione serverless: nessun daemon da mantenere, paghi per uso. Ideale se sei già su AWS. Lo svantaggio è la latenza maggiore (polling ogni X secondi) e la dipendenza da un servizio esterno.
Regola pratica: inizia con database, passa a Redis quando il carico cresce. Non sottovalutare la configurazione del worker in produzione con Supervisor per mantenere il worker sempre attivo.
Creare e spedire un Job
Un job in Laravel è una classe che implementa l'interfaccia ShouldQueue.
php artisan make:job ProcessOrder
La struttura base:
<?php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
public function handle()
{
// Logica pesante: calcoli, email, notifiche
$this->order->markAsProcessed();
// Invia email di conferma
Mail::to($this->order->user)->send(new OrderConfirmation($this->order));
}
}
Per spedirlo in coda:
Sponsored Protocol
ProcessOrder::dispatch($order);
Se vuoi eseguirlo subito senza coda (utile per test):
ProcessOrder::dispatchSync($order);
Oppure con ritardo:
ProcessOrder::dispatch($order)->delay(now()->addMinutes(10));
Dati da serializzare
Quando un job viene messo in coda, Laravel serializza le proprietà pubbliche e usa SerializesModels per i modelli Eloquent. Questo significa che il modello viene recuperato dal database al momento dell'esecuzione, non al momento della dispatch. Così eviti che i dati diventino obsoleti.
Il worker: chi esegue i lavori
Il worker è un processo PHP che ascolta la coda ed esegue i job. Lo avvii con:
php artisan queue:work
Opzioni fondamentali:
- --queue=high,default,low: priorità delle code
- --tries=3: numero di tentativi massimi
- --delay=5: secondi di attesa prima di riprovare un job fallito
- --sleep=3: secondi di pausa quando non ci sono job (per risparmiare CPU)
In produzione, non lanciare queue:work a mano. Usa Supervisor per mantenerlo sempre attivo. Un esempio di configurazione:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=forge
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/worker.log
Con numprocs=4 hai 4 worker in parallello sulla stessa coda. Aumentali quando il carico sale.
Sponsored Protocol
Gestire i fallimenti: tentativi, backoff e dead letter
I job falliscono. Può essere un errore temporaneo (database down, API esterna timeout) o permanente (dati corrotti, logica sbagliata). Laravel fornisce strumenti per gestire entrambi i casi.
Tentativi e backoff
Imposta $tries nella classe job per il massimo numero di tentativi:
public $tries = 5;
Il backoff (intervallo tra un tentativo e l'altro) può essere fisso o con backoff dinamico se usi il trait InteractsWithQueue:
public function backoff()
{
return [10, 30, 60, 120, 300]; // secondi
}
Il primo tentativo dopo 10 secondi, poi 30, 60, ecc. Utile per API che impongono rate limiting.
Max exceptions
Se un job lancia eccezioni ma vuoi che venga ritentato solo dopo un certo numero di fallimenti specifici, usa $maxExceptions. Per esempio, se un servizio esterno è giù, non ha senso riprovare all'infinito.
RetryUntil
Puoi impostare una scadenza temporale massima per un job, dopo la quale viene definitivamente marcato come fallito:
public function retryUntil()
{
return now()->addMinutes(30);
}
Utile per job sensibili al tempo (es. conferma prenotazione).
Il metodo failed()
Sovrascrivi il metodo failed per eseguire azioni quando un job esaurisce i tentativi (es. notificare l'amministratore, registrare un log, scrivere in una tabella di errori).
public function failed(\Throwable $exception)
{
Log::error('Order processing failed: ' . $this->order->id);
// Invia una notifica a Slack o email
Notification::send(User::admin(), new JobFailedNotification($this->order));
}
Tabella failed_jobs
Per default, i job falliti vengono salvati nella tabella failed_jobs. Puoi visualizzarli con:
Sponsored Protocol
php artisan queue:failed
E ritentare tutti i falliti:
php artisan queue:retry all
Oppure eliminarli:
php artisan queue:flush
Questa tabella è il tuo "dead letter queue" di Laravel. Monitorala regolarmente. Noi, di Meteora Web, abbiamo visto aziende perderci ordini perché non controllavano i job falliti. Un crontab che invia un report giornaliero dei failed jobs è una buona pratica.
Middleware per job: log, rate limit, blocchi
Laravel permette di applicare middleware ai job, esattamente come per le route. Alcuni utili built-in:
ThrottlesExceptions: evita che uno stesso job fallisca ripetutamente a causa di eccezioni temporanee (es. 429 Too Many Requests).RateLimited: limita il numero di job eseguiti per unità di tempo, pratico per API con limiti.WithoutOverlapping: impedisce che lo stesso job (basato su chiave) venga eseguito concorrentemente. Utile per processi che non devono sovrapporsi, come la sincronizzazione con un sistema esterno.
Esempio con ThrottlesExceptions:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
public function middleware()
{
return [
(new ThrottlesExceptions(5, 10))->backoff(2),
];
}
Permette 5 tentativi in 10 minuti, con backoff di 2 secondi tra tentativi. Questo evita di sovraccaricare un servizio instabile.
Monitoraggio in produzione con Horizon
Se usi Redis, Laravel Horizon è un must. Fornisce una dashboard con metriche in tempo reale: job in esecuzione, completati, falliti. Puoi riavviare i worker, vedere il tempo medio di esecuzione, e impostare bilanciamento automatico tra code con priorità. Noi lo installiamo in ogni progetto che supera le centinaia di job al giorno. La configurazione di Horizon nel file config/horizon.php permette di definire "supervisori" e "code” con pesi diversi.
Sponsored Protocol
Errori comuni e come evitarli
- Mancata configurazione del worker in produzione: se usi solo
queue:workin un terminale, il worker muore appena chiudi la sessione. Usa sempre Supervisor. - Ignorare i job falliti: un job fallito non viene riprocessato se non lo ritenti. Imposta un alert (email, Slack) per i fallimenti.
- Serializzare oggetti non serializzabili: evita di passare risorse (file handle, connessioni) come proprietà dei job. Passa ID e recupera i modelli all'interno del
handle(). - Non testare i job in isolamento: usa
Queue::fake()nei test per assicurarti che il job venga dispatched, ma testa anche l'esecuzione effettiva con un test di integrazione. - Confondere dispatch e dispatchNow:
dispatchNowesegue il job sincrono, bypassando la coda. Usalo solo per test o in contesti dove la coda non è disponibile.
In sintesi — cosa fare adesso
- Identifica le operazioni lente nella tua applicazione (invio email, elaborazione file, chiamate API esterne).
- Crea un job per ciascuna e sposta la logica nel metodo
handle(). - Scegli il driver di coda: database per iniziare, Redis per produzione.
- Configura Supervisor per mantenere attivo il worker.
- Imposta
$triesebackoffsu ogni job. - Monitora la tabella
failed_jobse imposta notifiche per i fallimenti. - Valuta Horizon se usi Redis per un controllo più granulare.
Un sito non è un'abitudine, è uno strumento. Se le code sono ben configurate, il tempo di risposta scende, l'utente è contento e tu hai un'applicazione che scala. Noi abbiamo visto clienti passare da un tempo di caricamento di 12 secondi a meno di 1 secondo spostando tre job in coda. I numeri parlano.