Este artigo é a continuação do lab de visibility timeout no SQS.
No primeiro lab eu tentei fabricar uma duplicata atrasando o relógio da fila. Não funcionou. A Lambda estende o visibility timeout enquanto processa. O alvo era o relógio. O alvo errado.
O que ficou em aberto era outra coisa: o produtor. E se eu mesmo mandar o mesmo pedido duas, quatro, dez vezes?
Foi isso que eu fiz. Abaixo está o que subi, por que subi, e o que cada print mostra.
O que eu queria descobrir
O Amazon SQS (Simple Queue Service) é uma fila. Você coloca uma mensagem. Um consumidor tira a mensagem e trabalha.
Fila standard entrega pelo menos uma vez. A documentação da Lambda diz a mesma coisa do outro lado: o event source mapping (a ligação fila → função) também processa pelo menos uma vez. O código precisa ser idempotente: processar o mesmo pedido uma vez ou dez vezes deixa o mesmo resultado.
Isso cobre a mensagem. Não cobre o pedido.
Imagine o checkout. O usuário clica em pagar. A rede falha. Ele clica de novo. O backend manda duas mensagens. O SQS aceita as duas. Cada uma ganha um MessageId novo. Para a fila, são dois recados. Para o caixa, é o mesmo ORDER-003 de 150 reais.
Pergunta do lab:
Se várias mensagens representam o mesmo pedido, o worker cobra uma vez ou cobra cada mensagem?
Dois identificadores. Não misture.
| Identificador | Quem cria | O que significa |
|---|---|---|
MessageId |
O SQS, a cada SendMessage
|
Aquela cópia na fila |
orderId |
Você, no JSON do body | O pedido de negócio |
O MessageId nomeia a mensagem. O orderId nomeia o pedido. Idempotência neste lab trava o pedido, não a mensagem.
O que eu subi na AWS
Tudo em us-east-1. Sem fila FIFO. Sem gateway de pagamento. A "cobrança" é um log chamado charge_applied. Se esse log aparece, o lab considera que o caixa rodou.
PowerShell (send.ps1)
|
v
SQS standard: aws-lab-sqs-idempotency
|
| uma mensagem por invocação (batch size 1)
v
Lambda: aws-lab-sqs-idempotency-worker
|
+-- CloudWatch Logs (o que aconteceu)
+-- DynamoDB (a trava do orderId)
| Peça | Nome | Para que serve |
|---|---|---|
| Fila | aws-lab-sqs-idempotency |
Recebe as cópias do pedido |
| Função | aws-lab-sqs-idempotency-worker |
Lê a fila e "cobra" |
| Tabela | aws-lab-sqs-idempotency |
Guarda o orderId com um write condicional |
| Logs | /aws/lambda/aws-lab-sqs-idempotency-worker |
Evidência no Insights |
A função tem um interruptor:
-
IDEMPOTENCY=0: chegou mensagem, cobra. Sem perguntar nada. -
IDEMPOTENCY=1: antes de cobrar, tenta gravar oorderIdno DynamoDB. Se o item já existe, não cobra.
Por que batch size 1: cada mensagem vira uma invocação. Assim duas cópias podem correr ao mesmo tempo, em vez de cair no mesmo lote.
Por que a guarda não é um if
Este código parece idempotência e não é:
if (exists) {
return;
}
Duas Lambdas leem ao mesmo tempo. As duas veem "não existe". As duas cobram. As duas gravam depois. A verificação e a reserva precisam ser a mesma operação.
No DynamoDB isso é um PutItem com condição: "grave este orderId só se ele ainda não existir".
await ddb.send(
new PutItemCommand({
TableName: tableName,
Item: {
orderId: { S: orderId },
status: { S: "CLAIMED" },
messageId: { S: messageId },
requestId: { S: requestId },
claimedAt: { S: new Date().toISOString() },
},
ConditionExpression: "attribute_not_exists(orderId)",
}),
);
Leitura do código, em português:
- Tente criar o item com PK
orderIde statusCLAIMED. - Se ninguém gravou essa PK ainda, você ganhou (
claim_won). Aí você cobra e marcaCOMPLETED. - Se a PK já existe, o DynamoDB recusa com
ConditionalCheckFailedException. Você perdeu (claim_lost), logaskip_duplicatee não cobra.
CLAIMED é reserva, não recibo. Eu não marco COMPLETED antes de cobrar. Se eu marcasse "já processado" e a cobrança falhasse, o retry pularia um pedido que nunca saiu.
Experimento 1: interruptor desligado
O que eu fiz: subi o lab com IDEMPOTENCY=0 e mandei mensagens do mesmo pedido.
Por que: para ver o comportamento sem trava. Se o worker cobra cada mensagem, o Insights tem que mostrar mais cobranças do que pedidos.
O que eu vi no Logs Insights (Analytics de logs, visão Tabelas de pesquisa), numa janela em que só existem três tipos de evento:
| eventType | O que significa | n |
|---|---|---|
process_start |
A Lambda começou a tratar a mensagem | 4 |
charge_applied |
O caixa rodou | 4 |
process_end |
A invocação terminou | 4 |
Não tem claim_won nem skip_duplicate. A guarda estava desligada.
Olhe a tabela de baixo: três linhas, todas com 4. Quatro invocações, quatro cobranças.
Depois eu contei só o efeito. A consulta pega charge_applied e pergunta: quantas cobranças, quantos orderId distintos, quantos MessageId distintos?
fields @timestamp, @message
| filter @message like /"type":"charge_applied"/
| parse @message /"orderId":"(?<orderId>[^"]+)"/
| parse @message /"messageId":"(?<messageId>[^"]+)"/
| stats count(*) as charges, count_distinct(orderId) as pedidos, count_distinct(messageId) as mensagens
| charges | pedidos | mensagens |
|---|---|---|
| 4 | 1 | 4 |
Quatro MessageId. Um orderId. Quatro cobranças. A fila tratou quatro recados. O caixa tratou o mesmo pedido quatro vezes.
O mesmo número aparece em outro recorte da mesma consulta.
Para o SQS, o teste passou: quatro mensagens, quatro invocações, zero erro. Para o negócio, o cliente foi cobrado quatro vezes.
O que idempotência quis dizer aqui
Caminho que eu usei:
orderId (o pedido) → mensagem (MessageId) → Lambda → efeito (charge_applied)
Idempotente: aplicar ORDER-003 uma vez ou dez vezes deixa uma cobrança de 150.
Não é "o SQS joga a segunda mensagem fora". O SQS pode (e neste lab, deve) entregar as dez. O destino é que recusa o segundo efeito.
Experimento 2: interruptor ligado, dez cópias
O que eu fiz:
-
destroydo lab antigo edeployde novo. - Esperei a Lambda ficar ativa e o event source mapping em
Enabled, batch size 1. -
.\set-idempotency.ps1 -Enabled 1. A variável da função ficouIDEMPOTENCY=1. - Mandei o mesmo pedido dez vezes:
.\send.ps1 -OrderId ORDER-003 -Copies 10
O body impresso:
{"orderId":"ORDER-003","customerId":"CUSTOMER-123","amount":150.00}
Dez linhas. Dez MessageId diferentes. orderId=ORDER-003 em todas.
O que observar: a fila não juntou nada. Dez recados. Um pedido.
Por que dez, e não duas: para forçar várias invocações no mesmo orderId com a guarda ligada.
O que o Insights mostrou nesse pedido
Filtrei orderId = "ORDER-003". Vieram 20 correspondências, nestes tipos:
process_start, claim_lost, skip_duplicate, process_end.
Leia assim: a mensagem chegou, tentou gravar a PK, perdeu, logou skip e encerrou. Sem nova cobrança nesse caminho.
A linha a linha é o mesmo filtro, evento por evento. Só ORDER-003. claim_lost e skip_duplicate andam juntos. Os MessageId mudam. O pedido não.
O que a tabela mostrou nesse pedido
O get-item de ORDER-003 devolveu um item:
| Campo | Valor | Por que importa |
|---|---|---|
orderId |
ORDER-003 |
A chave do pedido |
status |
COMPLETED |
A reserva virou recibo |
messageId / completedMessageId
|
73fca7ac-78e9-4ea4-a1bf-71a92e2fbda8 |
É o envio 1/10 daquela corrida |
claimedAt |
2026-09-20T20:58:23.240Z |
Quando a primeira cópia ganhou o PutItem |
completedAt |
2026-09-20T20:58:28.142Z |
Cerca de 5 s depois. O worker espera 4 s (WORK_MS=4000) e então marca completed |
| scan | 1 item | Não nasceu um item por mensagem |
A fila aceitou dez MessageId. O destino ficou com um ORDER-003 em COMPLETED. As cópias no Insights perderam o claim e não cobraram.
Os prints do grupo inteiro (com a guarda já visível)
No log group sem filtrar orderId, numa janela em que os eventos de claim já existem:
| eventType | n |
|---|---|
process_end |
6 |
charge_applied |
5 |
skip_duplicate |
1 |
claim_lost |
2 |
process_start |
2 |
claim_won |
1 |
Isso mostra a guarda em ação no grupo: alguém ganhou o claim, alguém perdeu, alguém pulou a cobrança.
A consulta de cobranças no grupo inteiro, recorte mais largo: 7 charges, 3 pedidos, 7 mensagens. São vários orderId no mesmo log group.
Lado a lado
| Sem guarda | Com guarda, pedido ORDER-003
|
|
|---|---|---|
| O que eu mandei | várias mensagens do mesmo pedido | 10 SendMessage com o mesmo ORDER-003
|
| O que a fila fez | aceitou cada MessageId
|
aceitou cada MessageId
|
| O que o Insights mostrou | 4 start, 4 charge, 4 end | 20 eventos de start / claim_lost / skip_duplicate / end |
| O que a tabela mostrou | a guarda estava desligada | um item ORDER-003 / COMPLETED
|
| Efeito no caixa | 4 cobranças para 1 orderId
|
um pedido reservado e concluído; as cópias visíveis não cobraram |
A fila fez o trabalho dela nas duas vezes. A diferença está no destino.
O que este lab não promete
- SQS standard não vira exactly-once porque você usou Lambda. Continua at-least-once.
-
if (exists) returnnão é a guarda. Duas leituras ao mesmo tempo furam isso. - FIFO com
MessageDeduplicationIdajuda a não enviar a mesma mensagem de novo por alguns minutos. Não substitui a tabela no destino quando você quer enfileirar o mesmo pedido várias vezes.
O que eu levo
-
MessageIdé a mensagem.orderIdé o pedido. A trava vai no pedido. - Sem guarda, quatro mensagens do mesmo
orderIdforam quatrocharge_applied. - A reserva é o
PutItemcomattribute_not_exists(orderId). Quem perde logaclaim_losteskip_duplicatee não cobra. - Idempotência neste desenho é: a fila pode entregar dez vezes; o caixa do pedido roda uma.








Top comments (0)