La plupart des systèmes « événementiels » finissent dans les douleurs classiques du distribué parce que deux primitives ne sont pas respectées : la backpressure et l'idempotence. Voici comment nous combinons Lambda et SQS dans des architectures qui survivent à la production.
Le pattern de référence
Au cœur de chaque système événementiel que nous livrons :
Producer → SNS/EventBridge → SQS → Lambda → Downstream
↘ DLQ
Chaque flèche est un contrat. Chaque composant a une seule responsabilité. Voyons pourquoi.
Pourquoi SNS ou EventBridge en amont
SQS est du point à point : une file, un consommateur logique. Si deux services ont besoin du même événement (journal d'audit et notification par e-mail), vous ne voulez pas que les producteurs dupliquent la publication. Placez SNS ou EventBridge en amont : les deux diffusent les événements vers plusieurs files SQS, avec un filtrage par abonné.
- SNS : plus simple, moins cher à fort volume, basé sur des topics.
- EventBridge : un routage plus riche (filtrage sur le contenu, schema registry, livraison cross-account). Légèrement plus cher.
Pourquoi SQS au milieu
Même si Lambda propose des intégrations directes avec SNS et EventBridge, nous plaçons presque toujours SQS entre les deux pour les charges de production. Les raisons :
- Backpressure. Si le consommateur ralentit, les messages s'accumulent dans SQS. Aucun signal de backpressure en intégration directe : Lambda est throttlé et SNS/EventBridge réessaie agressivement.
- Rejeu. En intégration directe, un échec Lambda est routé vers une destination, mais une fois traité, envolé. Une DLQ SQS vous donne une archive rejouable.
- Batching. Lambda lit SQS par lots allant jusqu'à 10 000 messages en Standard (fenêtre de batching requise, 6 Mo max par lot) ou 10 en FIFO. Bien plus efficace par invocation.
- Déploiements découplés. Livrez un changement côté consommateur avec zéro impact sur les producteurs.
L'idempotence est la première règle
SQS livre en at-least-once. SNS et EventBridge font de même. Cela signifie que chaque consommateur verra de temps en temps le même message deux fois. Si votre handler n'est pas idempotent, votre système abrite un bug de données en sursis.
Les stratégies que nous utilisons :
- Écritures conditionnelles :
PutItemDynamoDB avecattribute_not_exists(pk)ouConditionExpression. - Tables de clés d'idempotence : l'utilitaire d'idempotence de Powertools stocke l'ID de l'événement et le résultat dans DynamoDB.
- Idempotence naturelle : concevez les événements pour que les rejouer soit sans risque (par exemple « passer le statut à X » plutôt que « incrémenter le compteur »).
// Powertools idempotency in a handler (TypeScript)
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
const persistence = new DynamoDBPersistenceLayer({ tableName: 'IdempotencyStore' });
export const handler = makeHandlerIdempotent(async (event) => {
// guaranteed to run once per event, even on SQS redelivery
await processOrder(event);
}, { persistenceStore: persistence });
Partial batch response
Par défaut, si un message d'un lot échoue, les dix sont retentés, y compris les neuf qui ont réussi. Activez toujours ReportBatchItemFailures et renvoyez les IDs des messages en échec :
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
await process(record);
} catch (e) {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
DLQ + rejeu : une fonctionnalité, pas un filet de secours
Traitez la DLQ comme une partie de votre boucle d'exploitation normale :
- Alarme sur une profondeur de DLQ > 0.
- Livrez un petit outil de rejeu qui redéverse la DLQ vers la file principale après un correctif, construit à l'avance pour ne pas avoir à l'écrire à 3 h du matin.
- Taguez les messages de la DLQ avec l'erreur et le nombre de tentatives pour pouvoir les grouper et les trier rapidement.
Observabilité : le minimum viable
Les événements que vous devez pouvoir voir :
- Le taux d'émission des producteurs, par type d'événement.
- La profondeur de la file et l'âge du message le plus ancien : l'indicateur qui alerte en retard.
- Le nombre d'invocations, la durée et le taux d'erreur du consommateur.
- La profondeur de la DLQ : le canari.
- La latence de bout en bout : le temps entre l'émission de l'événement et l'écriture en aval.
Un système qu'on ne voit pas est un système qu'on ne peut pas exploiter. L'observabilité n'est pas une phase 2 ; elle se livre avec la phase 1.
Les modes de défaillance et comment concevoir autour
- Messages empoisonnés. DLQ + max receive count. Rejeu après correctif.
- Pannes en aval. SQS absorbe. Surveillez l'âge de la file ; c'est votre alerte.
- Bugs du consommateur. Déployez le consommateur, les messages s'accumulent sans dégât, drainez après correctif.
- Producteurs incontrôlés. La reserved concurrency sur le consommateur limite le rayon d'impact.
- Traitements en double. L'idempotence partout, toujours.
Un pipeline d'événements qui perd des messages ? Décrivez votre architecture : diagnostic d'une page sous 48 h.
Recevoir mon diagnostic →La liste courte
- SNS ou EventBridge → SQS → Lambda. Le pattern par défaut.
- Des consommateurs toujours idempotents.
- La partial batch response est le minimum syndical.
- DLQ + outil de rejeu, construits à l'avance.
- Alarme sur l'âge de la file et la profondeur de la DLQ, pas sur le taux d'erreur des invocations.
- Observez la latence de bout en bout, pas seulement celle de chaque composant.