Una coda asincrona evita che una richiesta web attenda il completamento di attività costose, ma da sola non risolve la competizione tra job. Il problema si presenta quando un'importazione, una rielaborazione o una campagna crea migliaia di messaggi e occupa tutti i consumer. Un'azione con impatto immediato —confermare un ordine, riservare l'inventario, bloccare un account o inviare una notifica transazionale— rimane dietro a lavoro che può attendere.
Gestire le priorità nelle code PHP non consiste soltanto nell'aggiungere un campo numerico al messaggio. È una decisione architetturale che deve riflettere il flusso di business, proteggere dipendenze limitate e mantenere un comportamento prevedibile quando il carico aumenta.
Classificate il lavoro per impatto, scadenza e costo

Prima di creare code, costruite un inventario dei job asincroni. Per ciascuno, identificate chi lo avvia, quale dipendenza utilizza, quanto dura solitamente, quale scadenza di business ha e cosa accade se viene ritardato. L'urgenza non equivale necessariamente all'importanza: una riconciliazione finanziaria può essere molto importante, ma tollerare diverse ore di attesa; una convalida del pagamento può richiedere una risposta rapida anche se la sua esecuzione è breve.
Una classificazione utile include solitamente quattro classi di servizio:
- Critica: azioni che proteggono denaro, sicurezza, coerenza o impegni immediati. Devono avere un obiettivo di attesa molto basso e capacità riservata.
- Interattiva: lavoro avviato da una persona o necessario per completare un'esperienza prossima al tempo reale, come generare un documento richiesto dall'applicazione.
- Differita: attività necessarie, ma senza una scadenza immediata, come sincronizzazioni periodiche, riepiloghi o aggiornamenti degli indici.
- Massiva: importazioni, migrazioni, reindicizzazioni, campagne e rielaborazioni. Il loro volume o costo richiede di limitarne il ritmo anche quando non esiste altro carico.
Registrate anche il costo per job. Un messaggio che chiama un'API con una quota limitata, esegue una query intensiva o elabora un file di grandi dimensioni non deve competere nello stesso modo di un breve aggiornamento locale. La classe di servizio deve esprimere la scadenza e il tipo di pressione che il job genera sul sistema.
Separate le code quando avete bisogno di un isolamento reale
Un'unica coda con priorità può essere adatta se i job hanno un'esecuzione omogenea, usano le stesse dipendenze e il transport offre una priorità affidabile. Tuttavia, l'ordine di prelievo non garantisce di per sé che esista capacità: un job massivo già in esecuzione continuerà a occupare un worker, una connessione o una quota esterna.
Separate le code quando esiste uno di questi limiti:
- I job critici e massivi hanno obiettivi di attesa chiaramente diversi.
- Un tipo di job accede a una dipendenza fragile o soggetta a limiti di frequenza, come un'API di pagamenti, email o ERP.
- La durata è molto disomogenea e i job lunghi trattengono i processi troppo a lungo.
- È richiesto un controllo indipendente di deploy, sospensione, retry o scalabilità.
- Un errore o un input anomalo di un flusso non deve degradare un altro flusso.
In un'applicazione PHP, il pattern più leggibile consiste solitamente nell'instradare i messaggi verso code esplicite, per esempio critical, interactive, deferred e bulk. Il componente di messaggistica può essere Symfony Messenger, Laravel Queues o un'integrazione propria con il broker scelto; il principio non dipende dal framework. La priorità all'interno di una coda può integrare questa separazione per ordinare job simili, non sostituire l'isolamento tra classi incompatibili.
Definite capacità riservata e concorrenza massima
Assegnate consumer per classe e stabilite sia minimi sia massimi operativi. La coda critica necessita di capacità che non possa essere assorbita dalle importazioni. Quella massiva, invece, deve avere un massimo di concorrenza per non saturare database, CPU, storage o provider esterni.
Evitate di configurare tutti i worker affinché leggano da tutte le code con preferenza assoluta per quella critica. Questo approccio può lasciare capacità inutilizzata se i consumer riservati non possono prendere altri job, oppure provocare inanizione se possono farlo senza regole. Un'alternativa pratica consiste nel combinare:
- Worker dedicati a critica e interattiva.
- Worker condivisi che gestiscano differita e massiva secondo quote.
- Limiti per tipo di dipendenza, non solo per numero totale di processi.
- Scalabilità basata sulla profondità della coda e sull'età dei messaggi, non esclusivamente sull'uso della CPU.
Il numero corretto non è universale. Deve partire dalla concorrenza tollerata dal database e dalle API, dalla durata osservata e dall'obiettivo di attesa di ogni classe.
Evitate l'inanizione e applicate backpressure
Dare preferenza all'urgente non significa che il lavoro differito non debba mai terminare. Se ci sono sempre messaggi critici, una politica di priorità rigorosa può produrre inanizione: i job di livello inferiore invecchiano indefinitamente. Stabilite una regola di fairness misurabile, come elaborare una quota di messaggi differiti dopo un numero limitato di messaggi critici, oppure riservare una piccola frazione di capacità al lavoro non urgente.
La regola deve rispettare i limiti delle dipendenze. Se critica e massiva scrivono nella stessa tabella con lock costosi, eseguirle in parallelo può peggiorare la latenza. In questo caso, la quota deve essere applicata alla risorsa condivisa oppure conviene riprogettare il lavoro in batch più piccoli.
Il backpressure si presenta quando entra più lavoro di quanto se ne possa completare. Non si risolve aumentando indefinitamente i worker. Definite come reagire:
- Limitate dimensione, frequenza o concorrenza delle importazioni alla fonte.
- Suddividete i batch in unità riprendibili e controllate quante ne vengono pubblicate contemporaneamente.
- Posticipate il lavoro differito con una pianificazione esplicita quando la coda o una dipendenza supera una soglia.
- Rispettate le risposte di rate limit con pause e retry differiti, invece di retry immediati.
- Comunicate al prodotto quando un'operazione viene accettata per l'elaborazione e quando è effettivamente completata.
È importante distinguere l'accettazione dall'esecuzione: restituire che un'importazione è stata ricevuta non implica che possa iniziare immediatamente. Questa trasparenza evita che una modifica tecnica venga interpretata come una promessa di disponibilità istantanea.
Controllate retry, lentezza e idempotenza
I retry consumano capacità e possono diventare un carico prioritario accidentale. Classificate gli errori tra transitori e permanenti. Un'interruzione temporanea della rete può giustificare un retry con attesa crescente e dispersione temporale; una convalida non valida, una risorsa inesistente o una credenziale revocata deve andare in un circuito di revisione, non essere ripetuta senza fine.
Stabilite un tempo massimo di esecuzione per tipo di job. Un job lento non deve trattenere un worker indefinitamente. Se può essere suddiviso, elaborate pagine, file o segmenti in messaggi indipendenti che registrino l'avanzamento. In caso contrario, usate limiti rigorosi, cancellazione sicura e una procedura per esaminare i job esauriti.
La priorità aumenta il rischio di ripetere effetti quando un producer reinvia un messaggio o un consumer fallisce dopo aver chiamato un'API esterna. Progettate handler idempotenti: usate una chiave stabile dell'operazione, persistete lo stato della transizione e fate in modo che elaborare due volte abbia lo stesso effetto di business che elaborare una sola volta. La deduplicazione del broker può ridurre i duplicati, ma non sostituisce l'idempotenza nell'applicazione né nelle integrazioni esterne.
Osservate l'attesa, non solo la dimensione della coda
Una coda breve può nascondere un problema se i suoi messaggi più vecchi attendono troppo a lungo o se i consumer falliscono costantemente. Misurate per classe di servizio l'età del messaggio più vecchio, il tempo dalla pubblicazione all'avvio, la durata di esecuzione, la percentuale di errori, retry e job inviati in revisione.
Completate queste metriche con concorrenza attiva, profondità, tasso di entrata e uscita, uso delle connessioni, tempi delle dipendenze e rate limit ricevuti. I segnali più utili sono: l'attesa critica supera il suo obiettivo, la coda massiva cresce mentre la sua quota è limitata, i retry dominano il traffico o la capacità riservata è inutilizzata durante i picchi di un'altra classe.
Configurate avvisi sulle tendenze e sugli obiettivi di servizio, non solo su un numero fisso di messaggi. Mille messaggi possono essere normali in un'importazione; dieci possono essere gravi se appartengono a conferme d'ordine che attendono da diversi minuti.
Esempio di flusso condiviso e checklist di implementazione

Immaginate una piattaforma che elabora ordini urgenti, notifiche e un'importazione massiva di catalogo. Gli ordini vengono instradati a critical; le notifiche transazionali, a interactive; e l'importazione viene frammentata in pagine inviate a bulk. I worker degli ordini hanno capacità riservata. L'importazione ha concorrenza limitata e riduce il proprio ritmo se aumenta la latenza del database. Le notifiche rispettano la quota del provider, con retry differiti. Se un processo si ripete, la chiave dell'operazione evita di creare due prenotazioni o di inviare due cambi di stato.
Per introdurre questo modello in un'applicazione esistente:
- Inventariate gli handler e assegnate una classe di servizio basata su scadenza, impatto e dipendenza.
- Misurate durata, attesa ed errori prima di modificare l'instradamento.
- Separate prima i flussi critici da quelli massivi e riservate capacità minima.
- Definite limiti di concorrenza per dipendenza e politiche di backpressure.
- Rendete idempotenti gli effetti di business e limitate retry e tempi di esecuzione.
- Testate picchi di carico, indisponibilità dei provider e un input massivo prima di attivare la nuova ripartizione.
- Rivedete periodicamente quote e classi: una priorità è una politica di business che cambia con il prodotto.
Il risultato desiderato non è che tutto sia prioritario, ma che ogni job riceva capacità e una scadenza coerenti, senza trasformare un'operazione ad alto volume in un blocco per il resto del business.



