Tre domande prima di scegliere un servizio
Dentro AWS ci sono tre modi diffusi per far viaggiare un messaggio, e la scelta si riduce a tre domande. Il messaggio deve essere lavorato da un solo consumatore tra molti identici? Allora è una coda: SQS. Deve arrivare a tutti quelli che lo vogliono? Allora è un topic: SNS. I destinatari sono molti, cambiano spesso e devono scegliere in base al contenuto? Allora è un bus con regole: EventBridge, descritto nell'articolo precedente. Si combinano: il pattern più comune è un topic che alimenta una coda per ogni consumatore.
Qui non c'è un account AWS: costruisco in piccolo i comportamenti che fanno decidere (visibility timeout, ricezioni ripetute, DLQ, ordinamento per gruppo, deduplicazione, fan-out con filtro) e li eseguo con un tempo virtuale, poi riporto dalla guida ufficiale i numeri che contano, consultata il 2026-11-12. È un modello delle regole documentate, non il servizio: ogni volta che il servizio vero si comporta diversamente, o non l'ho potuto provare, lo scrivo.
La coda: cosa fa un messaggio dopo che qualcuno lo riceve
In SQS un messaggio non sparisce quando lo ricevi: diventa invisibile per il visibility timeout (30 secondi per default, al massimo 12 ore secondo la guida), e torna disponibile se nel frattempo non lo cancelli. È un contratto semplice: il consumatore dice di aver finito cancellando il messaggio; se non lo fa, SQS suppone che sia morto e lo riconsegna. Il modello sotto implementa questa regola, la consegna alla DLQ dopo un numero massimo di ricezioni e le due varianti FIFO (ordine per gruppo, deduplicazione per cinque minuti). Il tempo è un contatore, così le uscite sono riproducibili.
// Modello minimo di una coda con visibility timeout, DLQ e modalita' FIFO. Il tempo e' virtuale (secondi).
type Msg = { id: number; body: string; group?: string; ricevuto: number; visibileDa: number };
class Coda {
msgs: Msg[] = [];
private visti = new Map<string, number>(); // dedupId -> istante dell'invio (solo FIFO)
private n = 0;
nome: string;
private clock: { t: number };
private opz: { visibility?: number; fifo?: boolean; maxReceive?: number; dlq?: Coda };
constructor(nome: string, clock: { t: number }, opz: { visibility?: number; fifo?: boolean; maxReceive?: number; dlq?: Coda } = {}) {
this.nome = nome; this.clock = clock; this.opz = opz;
}
send(body: string, o: { group?: string; dedupId?: string } = {}): boolean {
if (this.opz.fifo && o.dedupId) {
const prima = this.visti.get(o.dedupId);
if (prima !== undefined && this.clock.t - prima < 300) return false; // finestra di 5 minuti
this.visti.set(o.dedupId, this.clock.t);
}
this.msgs.push({ id: ++this.n, body, group: o.group, ricevuto: 0, visibileDa: 0 });
return true;
}
receive(max = 10): Msg[] {
const out: Msg[] = [];
for (const m of [...this.msgs]) {
if (out.length >= max) break;
if (m.visibileDa > this.clock.t) continue; // ancora invisibile
// FIFO: finche' un messaggio del gruppo e' in volo, i successivi dello stesso gruppo restano fermi.
if (this.opz.fifo && this.msgs.some((x) => x.group === m.group && x.id < m.id)) continue;
if (this.opz.maxReceive !== undefined && m.ricevuto >= this.opz.maxReceive && this.opz.dlq) {
this.msgs = this.msgs.filter((x) => x !== m);
this.opz.dlq.msgs.push({ ...m, ricevuto: 0, visibileDa: 0 });
continue; // spostato nella DLQ, non consegnato
}
m.ricevuto++;
m.visibileDa = this.clock.t + (this.opz.visibility ?? 30);
out.push(m);
}
return out;
}
delete(m: Msg): void { this.msgs = this.msgs.filter((x) => x.id !== m.id); }
}
receive è il metodo che conta. Salta i messaggi ancora invisibili (visibileDa), conta una ricezione e
posticipa la visibilità di visibility secondi. Se la coda è FIFO, un messaggio non viene consegnato finché ne
esiste uno più vecchio dello stesso gruppo: così l'ordine per gruppo è garantito anche con più consumatori.
Se il messaggio ha
già raggiunto maxReceive ricezioni, finisce nella DLQ invece di essere consegnato. I messaggi del gruppo A
aspettano A1 anche se B è libero: i gruppi sono il modo di avere ordine e parallelismo insieme. Una differenza rispetto al
servizio vero: il momento esatto dello spostamento nella DLQ e le garanzie di ordine su carichi alti sono dettagli della
guida, non di questo modello.
Consegna ripetuta e messaggio velenoso
Il primo scenario dimostra perché un consumatore SQS deve essere idempotente: il lavoro dura 40 secondi e il timeout è 30, quindi il messaggio viene consegnato una seconda volta mentre il primo consumatore sta ancora lavorando. Succede ai consumatori lenti, non è un errore di SQS. Il secondo scenario mostra il rimedio per i messaggi che non si possono mai elaborare: dopo tre ricezioni il messaggio velenoso va nella DLQ e il messaggio buono non è mai stato bloccato.
const clock = { t: 0 };
// 1) Standard: il consumatore e' piu' lento del visibility timeout, il messaggio viene consegnato due volte.
const std = new Coda('std', clock, { visibility: 30 });
std.send('ordine-1');
const a = std.receive()[0]; // t=0: il consumatore A inizia (ci mettera' 40 s)
clock.t = 31;
const b = std.receive()[0]; // t=31: il timeout e' scaduto, B riceve lo stesso messaggio
console.log(`1) A ha ricevuto "${a.body}" a t=0, B lo riceve a t=31 (ricezioni: ${b.ricevuto})`);
// 2) Poison message: fallisce sempre, dopo maxReceive=3 ricezioni finisce nella DLQ.
clock.t = 0;
const dlq = new Coda('dlq', clock);
const q = new Coda('ordini', clock, { visibility: 30, maxReceive: 3, dlq });
q.send('veleno'); q.send('buono');
for (let giro = 1; giro <= 5; giro++) {
for (const m of q.receive()) {
if (m.body === 'buono') { q.delete(m); console.log(`2) t=${clock.t}: "buono" elaborato`); }
else console.log(`2) t=${clock.t}: "${m.body}" fallisce (ricezione ${m.ricevuto})`);
}
clock.t += 31;
}
console.log(`2) in coda: ${q.msgs.length}, in DLQ: ${dlq.msgs.map((m) => m.body)}`);
Nella prima riga di uscita B riceve a t=31 lo stesso messaggio che A aveva ricevuto a t=0, e il contatore di ricezioni vale 2.
Per il secondo scenario, veleno fallisce a t=0, 31 e 62; al giro successivo non viene più consegnato ed è nella
DLQ, mentre buono è stato elaborato subito.
Il messaggio può restare nella DLQ per essere analizzato o rigiocato,
cosa di cui si occupa l'articolo sulla dead letter queue. La
guida chiama maxReceiveCount il numero di volte in cui un consumatore può ricevere il messaggio prima che sia
spostato: con 1 un solo errore basta a mandarlo nella DLQ, e va scelto più alto per tollerare i guasti temporanei.
FIFO: ordine per gruppo e deduplicazione
La coda Standard garantisce, per la guida, consegna almeno una volta, con possibili copie e arrivi fuori
ordine, ed è la scelta di default. La coda FIFO aggiunge l'ordine entro un gruppo (MessageGroupId)
e la deduplicazione: la guida dice che un nuovo invio con lo stesso identificativo entro l'intervallo di cinque minuti non
introduce duplicati nella coda. Attenzione: questo non è «esattamente una volta» a livello di consumatore, perché un
consumatore lento può comunque ricevere due volte per via del timeout.
// 3) FIFO: ordine per gruppo e deduplicazione entro 5 minuti.
clock.t = 0;
const fifo = new Coda('ordini.fifo', clock, { fifo: true });
const esiti = [
fifo.send('A1', { group: 'A', dedupId: 'a1' }), fifo.send('A2', { group: 'A', dedupId: 'a2' }),
fifo.send('B1', { group: 'B', dedupId: 'b1' }), fifo.send('A1', { group: 'A', dedupId: 'a1' }),
];
console.log('3) invii accettati:', esiti);
const r1 = fifo.receive();
console.log('3) prima ricezione:', r1.map((m) => m.body));
r1.filter((m) => m.body === 'A1').forEach((m) => fifo.delete(m));
console.log('3) dopo la cancellazione di A1:', fifo.receive().map((m) => m.body));
I quattro invii restituiscono true, true, true, false: il secondo invio di A1 con lo stesso
dedupId viene ignorato. La prima ricezione consegna A1 e B1 ma non A2, che
aspetta la cancellazione di A1; dopo la cancellazione, A2 esce. Il prezzo del gruppo unico è il parallelismo: se tutti i
messaggi usano lo stesso gruppo, un solo messaggio alla volta è in lavorazione. La guida consiglia di usare un gruppo diverso
per ogni produttore se serve l'ordine rigoroso tra più produttori; scegli il gruppo in base all'entità che deve restare in
ordine, per esempio l'ordine di un cliente, non l'intero sistema.
Il topic: copiare il messaggio in tante code
Un topic SNS non conserva niente per i consumatori lenti: consegna in push a tutti gli iscritti. Per questo si accoppia a SQS: ogni consumatore ha la sua coda iscritta al topic, e la coda fa da buffer. La guida dice che i messaggi arrivano alle applicazioni che vogliono la notifica immediata e restano nella coda per quelle che li elaborano più tardi. Una sottoscrizione può avere una filter policy: un oggetto JSON che definisce quali messaggi riceve, valutato sugli attributi del messaggio o sul corpo secondo lo scope scelto; senza policy, la sottoscrizione riceve tutto.
// 4) SNS + SQS: un topic copia il messaggio nelle code iscritte il cui filtro combacia.
const inventario = new Coda('inventario', clock), notifiche = new Coda('notifiche', clock), audit = new Coda('audit', clock);
const sottoscrizioni = [
{ coda: inventario, filtro: { eventType: ['OrdineCreato'] } },
{ coda: notifiche, filtro: { eventType: ['OrdineCreato', 'OrdineSpedito'] } },
{ coda: audit, filtro: null }, // nessun filtro: riceve tutto
];
const pubblica = (eventType: string) => {
for (const s of sottoscrizioni) if (!s.filtro || s.filtro.eventType.includes(eventType)) s.coda.send(eventType);
};
['OrdineCreato', 'OrdineSpedito', 'OrdineAnnullato'].forEach(pubblica);
for (const s of sottoscrizioni) console.log(`4) ${s.coda.nome.padEnd(10)}`, s.coda.msgs.map((m) => m.body));
// Output (esecuzione dell'intero file):
// 1) A ha ricevuto "ordine-1" a t=0, B lo riceve a t=31 (ricezioni: 2)
// 2) t=0: "veleno" fallisce (ricezione 1)
// 2) t=0: "buono" elaborato
// 2) t=31: "veleno" fallisce (ricezione 2)
// 2) t=62: "veleno" fallisce (ricezione 3)
// 2) in coda: 0, in DLQ: veleno
// 3) invii accettati: [ true, true, true, false ]
// 3) prima ricezione: [ 'A1', 'B1' ]
// 3) dopo la cancellazione di A1: [ 'A2' ]
// 4) inventario [ 'OrdineCreato' ]
// 4) notifiche [ 'OrdineCreato', 'OrdineSpedito' ]
// 4) audit [ 'OrdineCreato', 'OrdineSpedito', 'OrdineAnnullato' ]
Le tre sottoscrizioni del modello sono quelle dell'architettura tipica: inventario vuole solo gli ordini creati,
notifiche due tipi, audit tutto perché non ha filtro. L'uscita mostra le copie: tre messaggi
pubblicati, una copia in un caso, due in un altro, tre nel terzo. Se sbagli un filtro, il messaggio non va a nessuna coda
senza che ci sia un errore da vedere, quindi conviene avere sempre una sottoscrizione di audit senza filtro. Nel modello il
filtro è un'uguaglianza su un valore; il servizio vero ha operatori più ricchi, descritti nella guida, ma meno di quelli di
EventBridge, che filtra su qualunque campo del JSON.
Cosa dice la guida: i numeri da conoscere
I numeri sotto vengono dalla documentazione ufficiale consultata il 2026-11-12; cambiano nel tempo e per tipo di coda, quindi ricontrollali sulla pagina delle quote prima di dimensionare.
- Il visibility timeout di default è 30 secondi, con massimo 12 ore dalla prima ricezione.
- Il long polling aspetta fino a 20 secondi: riduce le risposte vuote.
- Si possono inviare, ricevere o cancellare fino a 10 messaggi in una sola richiesta.
- Un messaggio può avere fino a 10 attributi e la dimensione massima è 1 MiB (1.048.576 byte).
- La conservazione di default è 4 giorni, e il ritardo di consegna arriva a 15 minuti.
- Una coda Standard ha un limite di circa 120.000 messaggi in volo.
Il numero che morde di più è il timeout, perché si regola sul tempo di elaborazione peggiore del consumatore, non sulla media: troppo basso produce le doppie consegne viste sopra, troppo alto ritarda il recupero quando un consumatore muore. Il limite dei messaggi in volo conta quando consumatori lenti e molti messaggi si sommano; gli altri valori sono parametri di progetto. Non riporto costi per milione di messaggi, latenza né throughput massimo: sono numeri che cambiano e che qui non posso né misurare né citare da una pagina verificata; per i costi vale la pagina dei prezzi di AWS.
Il confronto, senza cifre
| Domanda | SQS | SNS | EventBridge |
|---|---|---|---|
| Chi riceve | Un consumatore tra quelli collegati alla coda | Tutti gli iscritti, con filtro opzionale | I target delle regole che combaciano |
| Buffer per i consumatori lenti | Sì, è la sua funzione | No, consegna in push (si usa con SQS) | Retry per target, DLQ opzionale |
| Ordine | Per gruppo, nelle code FIFO | Nei topic FIFO | Dichiarato per il Custom Event Bus; per il Classic la pagina consultata non lo dichiara |
| Filtro | Nessuno | Su attributi o corpo | Su qualunque campo del JSON |
| Archivio e replay | No: un messaggio cancellato non si recupera | Non verificato in questa sede | Sì, documentato |
Leggi la tabella per riga più che per colonna. Il buffer è la riga che separa SQS da tutto il resto: nessuna altra combinazione ti dà una coda che assorbe un picco. L'ordine è la riga che si copia più spesso male: anche SNS ha una versione FIFO, e anche per EventBridge la guida distingue i due tipi di bus.
Il filtro è il segnale di scelta tra SNS ed EventBridge: se ti basta un attributo, il topic è più semplice; se devi guardare il contenuto, il bus. L'archivio con replay di EventBridge è una ragione per sceglierlo anche quando il filtro non serve, perché permette di alimentare un consumatore nuovo; per SNS non ho verificato cosa offra la guida attuale, e prima di decidere va controllato.
Scegliere, in pratica
Usa SQS quando il lavoro va diviso tra consumatori identici o deve assorbire un picco, o quando serve l'ordine per gruppo. Usa SNS con una coda per ogni consumatore quando lo stesso evento deve raggiungere più servizi e ognuno ha i suoi tempi. Usa EventBridge quando i destinatari cambiano spesso o devono scegliere in base ai dati, o quando serve il replay.
Lo stesso flusso può usarli tutti: un bus smista per contenuto verso una coda per consumatore, e la coda tiene il buffer e i ritentativi. Qualunque sia la scelta, il consumatore deve essere idempotente (si veda l'articolo sull'idempotenza), perché la consegna ripetuta è prevista dal servizio e non un incidente.
Che cosa sai fare adesso
Sai scegliere tra coda, topic e bus partendo da tre domande, sai predire che cosa accade a un messaggio quando il consumatore è lento o fallisce, e sai perché un filtro sbagliato fa sparire i messaggi senza errori. Sai quali numeri controllare sulla guida e quali non ti servono. Il passo successivo è la gestione dei fallimenti: la dead letter queue. Per le operazioni che più servizi devono completare insieme c'è la saga, e per l'intervallo in cui i dati divergono la consistenza eventuale.
Fonti
- Amazon SQS Developer Guide, Amazon SQS standard queues - consegna almeno una volta, copie multiple e ordine best-effort, consultata il 2026-11-12
- Amazon SQS, Visibility timeout - 30 secondi di default, 12 ore di massimo, consultata il 2026-11-12
- Amazon SQS, Dead-letter queues - definizione di
maxReceiveCounte redrive, consultata il 2026-11-12 - Amazon SQS, FIFO delivery logic - ordine per message group ID; exactly-once processing - intervallo di deduplicazione di cinque minuti, consultate il 2026-11-12
- Amazon SQS, Quotas related to messages - dimensione massima, conservazione, attributi e batch, consultata il 2026-11-12; short and long polling - attesa massima di 20 secondi
- Amazon SNS Developer Guide, Message filtering - filter policy su attributi o corpo, senza policy si riceve tutto, consultata il 2026-11-12; SQS come sottoscrittore - consegna in push dei topic e persistenza nella coda
Il modello (code.ts) è stato eseguito con Node.js v24.11.0 e il tempo è virtuale: le uscite sono reali per il modello, non misure di SQS o SNS. Nessun servizio AWS è stato chiamato.

Commenti
Caricamento commenti...
Accedi per commentare