Visibility Timeout no SQS: Por Que Suas Mensagens São Processadas Duas Vezes
Você está vendo a mesma mensagem sendo processada por dois consumidores diferentes, gerando duplicatas no banco de dados ou efeitos colaterais repetidos em produção. O problema quase sempre está no Visibility Timeout do SQS configurado abaixo do tempo real de processamento — e o SQS, por design, assume que a mensagem foi perdida e a reentrega.
TL;DR — Visibility Timeout no SQS
| Ponto | Detalhe |
|---|---|
| O que é | Janela de tempo em que uma mensagem fica invisível para outros consumidores após ser recebida |
| Padrão | 30 segundos |
| Faixa configurável | 0 segundos a 12 horas |
| Causa de duplicata | Processamento demora mais que o timeout — SQS reentrega a mensagem |
| Solução imediata | Aumentar o timeout ou chamar ChangeMessageVisibility durante o processamento |
| Garantia do SQS | At-least-once delivery — duplicatas são esperadas, idempotência é obrigatória |
Como o Visibility Timeout Funciona no SQS
Quando um consumidor chama ReceiveMessage, o SQS não remove a mensagem da fila — ele apenas a torna invisível por um período determinado pelo Visibility Timeout. Durante esse período, nenhum outro consumidor consegue ver ou receber aquela mensagem. Se o consumidor original deletar a mensagem com DeleteMessage antes do timeout expirar, tudo certo. Se não deletar a tempo, o SQS assume que o processamento falhou e torna a mensagem visível novamente para qualquer consumidor disponível.
Esse comportamento é intencional. O SQS é uma fila de entrega at-least-once — a garantia é que a mensagem será entregue pelo menos uma vez, não exatamente uma vez. A responsabilidade de lidar com duplicatas é do lado da aplicação, via idempotência.
- ReceiveMessage: Consumidor A busca a mensagem. O SQS inicia o contador do Visibility Timeout.
- Invisível para outros: Enquanto o timeout não expira, nenhum outro consumidor vê a mensagem.
- Caminho feliz: Consumidor A termina o processamento e chama
DeleteMessage. Mensagem removida da fila. - Timeout expirado: Se
DeleteMessagenão foi chamado a tempo, a mensagem volta a ficar visível. - Reentrega: Consumidor B (ou o próprio A em outra thread) recebe a mensagem novamente — duplicata.
Por Que o Visibility Timeout Padrão Causa Problemas em Produção
O valor padrão de 30 segundos parece razoável até você colocar em produção um worker que faz chamadas a APIs externas lentas, processa arquivos grandes no S3, ou executa queries pesadas. Em qualquer desses cenários, 30 segundos desaparecem rápido.
O erro clássico de diagnóstico é assumir que o problema é um bug no código do consumidor ou uma condição de corrida na aplicação. Você passa horas revisando locks e transações enquanto o SQS silenciosamente reentraga mensagens porque o timeout expirou. O log do consumidor B mostra o início do processamento da mesma mensagem sem nenhum erro — parece comportamento normal.
Pense no Visibility Timeout como um timer de microondas. Você colocou a comida para esquentar, mas saiu da cozinha. Quando o timer apita e você não voltou, outra pessoa assume que o microondas está livre e coloca a própria comida — agora as duas estão sendo esquentadas ao mesmo tempo.
Verificando a Configuração Atual do Visibility Timeout
Antes de alterar qualquer coisa, confirme o valor atual da fila e compare com o tempo médio de processamento real dos seus workers. Esses dois números precisam estar alinhados.
# Verificar atributos da fila, incluindo VisibilityTimeout
aws sqs get-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/minha-fila \
--attribute-names VisibilityTimeout ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible \
--region us-east-1
O campo ApproximateNumberOfMessagesNotVisible indica quantas mensagens estão atualmente dentro de um Visibility Timeout ativo — ou seja, sendo processadas (ou com timeout expirado mas ainda não reentregues). Se esse número for consistentemente alto e igual ao número de workers, está normal. Se for muito maior, mensagens estão acumulando em estado invisível sem serem deletadas.
Ajustando o Visibility Timeout na Fila
A correção mais direta é aumentar o Visibility Timeout para cobrir o pior caso de processamento com uma margem de segurança. A regra prática: meça o percentil 99 do tempo de processamento e multiplique por 1,5.
# Alterar o Visibility Timeout da fila para 5 minutos (300 segundos)
aws sqs set-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/minha-fila \
--attributes VisibilityTimeout=300 \
--region us-east-1
Essa alteração afeta todas as mensagens recebidas a partir desse momento. Mensagens já em processamento mantêm o timeout original com que foram recebidas.
Estendendo o Timeout Dinamicamente com ChangeMessageVisibility
Aumentar o timeout fixo da fila resolve o caso médio, mas não o caso variável — um job que normalmente termina em 2 minutos mas ocasionalmente demora 8. Para isso, a abordagem correta é estender o timeout programaticamente durante o processamento usando ChangeMessageVisibility.
O padrão típico é um heartbeat: uma thread separada no worker chama ChangeMessageVisibility periodicamente enquanto o processamento principal ainda está em andamento.
- Worker recebe a mensagem com Visibility Timeout inicial de, por exemplo, 60 segundos.
- Thread de heartbeat é iniciada em paralelo, configurada para chamar
ChangeMessageVisibilitya cada 45 segundos. - Cada chamada de heartbeat reseta o timeout para mais 60 segundos a partir daquele momento.
- Processamento termina: thread principal chama
DeleteMessage, thread de heartbeat é encerrada. - Se o worker morrer: heartbeat para, timeout expira naturalmente, SQS reentraga a mensagem.
# Estender o Visibility Timeout de uma mensagem específica para mais 120 segundos
aws sqs change-message-visibility \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/minha-fila \
--receipt-handle 'AQEBwJnKyrHigUMZj6reyNurzbEI...' \
--visibility-timeout 120 \
--region us-east-1
O receipt-handle é retornado pelo ReceiveMessage e é único por recebimento — não é o Message ID. Se a mensagem for reentregada, o receipt handle muda.
Visibility Timeout em Filas FIFO
Em filas FIFO, o Visibility Timeout funciona da mesma forma, mas com uma implicação importante para o ordenamento por grupo. Enquanto uma mensagem de um MessageGroupId está dentro do Visibility Timeout (invisível), nenhuma outra mensagem do mesmo grupo é entregue. Isso garante o processamento em ordem dentro do grupo, mas significa que um timeout longo em uma mensagem lenta pode bloquear todo o grupo.
O Cenário Real: Diagnóstico de Duplicatas em Produção
Em um sistema de processamento de pedidos, começamos a ver pedidos sendo confirmados duas vezes para alguns clientes. O primeiro instinto foi procurar uma condição de corrida no código de confirmação — revisamos locks de banco de dados, transações, índices únicos. Tudo parecia correto.
O que os logs mostravam: dois ORDER_CONFIRMED events com timestamps separados por exatamente 30 segundos para os pedidos afetados. Trinta segundos — o valor padrão do Visibility Timeout.
O que estava acontecendo: pedidos de clientes com muitos itens disparavam chamadas síncronas para um serviço de estoque externo que às vezes demorava 35-40 segundos. O worker não terminava o processamento antes do timeout expirar. O SQS reentraga a mensagem, um segundo worker a recebia e completava o processamento antes do primeiro — que ainda estava esperando a resposta do serviço de estoque — terminar e tentar deletar a mensagem com um receipt handle já inválido.
A correção foi em duas partes: aumentar o Visibility Timeout para 3 minutos e adicionar uma chave de idempotência na tabela de pedidos para rejeitar confirmações duplicadas do mesmo order_id. O timeout resolve o caso normal; a idempotência protege contra qualquer caso residual.
Configurando a Dead Letter Queue para Mensagens com Falha Repetida
Aumentar o Visibility Timeout não resolve mensagens que falham consistentemente — elas continuarão sendo reenviadas indefinidamente. Para isso, configure uma Dead Letter Queue (DLQ) com um maxReceiveCount adequado. Após esse número de tentativas, o SQS move a mensagem para a DLQ automaticamente.
# Criar a Dead Letter Queue
aws sqs create-queue \
--queue-name minha-fila-dlq \
--region us-east-1
# Obter o ARN da DLQ criada
aws sqs get-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/minha-fila-dlq \
--attribute-names QueueArn \
--region us-east-1
# Configurar a fila principal com redrive policy apontando para a DLQ
aws sqs set-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/minha-fila \
--attributes '{"RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:123456789012:minha-fila-dlq\",\"maxReceiveCount\":\"5\"}" }' \
--region us-east-1
IAM: Permissões Mínimas para Operações de Visibility Timeout
Workers que precisam chamar ChangeMessageVisibility além das operações padrão de consumo precisam de permissão explícita. Segue a política mínima necessária:
🔽 Clique para expandir — Política IAM mínima para consumer com heartbeat
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SQSConsumerWithHeartbeat",
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:ChangeMessageVisibility",
"sqs:GetQueueAttributes"
],
"Resource": "arn:aws:sqs:us-east-1:123456789012:minha-fila"
}
]
}
sqs:GetQueueAttributes é útil para o worker verificar o timeout configurado na fila em tempo de inicialização e ajustar o intervalo do heartbeat dinamicamente.
Monitorando o Visibility Timeout com CloudWatch
Duas métricas do CloudWatch são diretamente relevantes para diagnosticar problemas de Visibility Timeout:
- ApproximateNumberOfMessagesNotVisible: mensagens atualmente dentro de um Visibility Timeout ativo. Um crescimento contínuo sem correspondência no throughput de deleção indica workers travados ou timeouts muito longos.
- NumberOfMessagesReceived vs. NumberOfMessagesDeleted: se a diferença entre essas duas métricas crescer ao longo do tempo, mensagens estão sendo recebidas mas não deletadas — timeout expirando antes do processamento terminar.
# Verificar métrica de mensagens não visíveis nos últimos 30 minutos
aws cloudwatch get-metric-statistics \
--namespace AWS/SQS \
--metric-name ApproximateNumberOfMessagesNotVisible \
--dimensions Name=QueueName,Value=minha-fila \
--start-time $(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 \
--statistics Maximum \
--region us-east-1
Próximos Passos e Referências sobre Visibility Timeout no SQS
O Visibility Timeout é um dos parâmetros mais impactantes do SQS e um dos mais frequentemente ignorados durante o desenvolvimento — o problema só aparece em produção quando o processamento fica mais lento que o esperado. A combinação de um timeout adequado, heartbeat para jobs de duração variável, idempotência na aplicação e DLQ configurada cobre a grande maioria dos cenários de duplicata.
- Meça o percentil 99 do tempo de processamento antes de definir o timeout
- Implemente idempotência independentemente do timeout — duplicatas residuais sempre existirão
- Configure alertas no CloudWatch para
ApproximateNumberOfMessagesNotVisiblecrescendo acima do esperado - Para jobs de duração imprevisível, prefira o padrão de heartbeat com
ChangeMessageVisibility - Consulte a documentação oficial do SQS sobre Visibility Timeout para detalhes sobre limites e comportamento de filas FIFO
Glossário — Termos Essenciais do SQS
| Termo | Definição |
|---|---|
| Visibility Timeout | Período em que uma mensagem fica invisível para outros consumidores após ser recebida via ReceiveMessage |
| Receipt Handle | Identificador único retornado por cada ReceiveMessage, necessário para DeleteMessage e ChangeMessageVisibility. Muda a cada reentrega. |
| At-least-once Delivery | Garantia do SQS Standard de que cada mensagem será entregue pelo menos uma vez, podendo haver duplicatas |
| Dead Letter Queue (DLQ) | Fila de destino para mensagens que excederam o número máximo de tentativas de processamento (maxReceiveCount) |
| Message Group ID | Atributo de filas FIFO que garante ordenamento e processamento sequencial dentro de um grupo |
Comentários
Postar um comentário