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

PontoDetalhe
O que éJanela de tempo em que uma mensagem fica invisível para outros consumidores após ser recebida
Padrão30 segundos
Faixa configurável0 segundos a 12 horas
Causa de duplicataProcessamento demora mais que o timeout — SQS reentrega a mensagem
Solução imediataAumentar o timeout ou chamar ChangeMessageVisibility durante o processamento
Garantia do SQSAt-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.

sequenceDiagram participant CA as Consumidor A participant SQS as SQS Queue participant CB as Consumidor B CA->>SQS: ReceiveMessage SQS-->>CA: Mensagem + Receipt Handle Note over SQS: Visibility Timeout iniciado (ex: 30s) Note over SQS: Mensagem invisível para outros alt Processamento dentro do timeout CA->>CA: Processa mensagem CA->>SQS: DeleteMessage(receiptHandle) SQS-->>CA: Mensagem removida else Timeout expira antes do Delete Note over SQS: Timeout expirou — mensagem volta a ficar visível CB->>SQS: ReceiveMessage SQS-->>CB: Mesma mensagem (nova entrega) CB->>CB: Processa mensagem novamente Note over CA,CB: DUPLICATA — dois consumidores processaram a mesma mensagem end
  1. ReceiveMessage: Consumidor A busca a mensagem. O SQS inicia o contador do Visibility Timeout.
  2. Invisível para outros: Enquanto o timeout não expira, nenhum outro consumidor vê a mensagem.
  3. Caminho feliz: Consumidor A termina o processamento e chama DeleteMessage. Mensagem removida da fila.
  4. Timeout expirado: Se DeleteMessage não foi chamado a tempo, a mensagem volta a ficar visível.
  5. 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.

sequenceDiagram participant W as Worker (Thread Principal) participant H as Heartbeat (Thread Paralela) participant SQS as SQS Queue W->>SQS: ReceiveMessage SQS-->>W: Mensagem (Timeout: 60s) W->>H: Inicia heartbeat a cada 45s loop A cada 45 segundos H->>SQS: ChangeMessageVisibility(+60s) SQS-->>H: Timeout resetado end W->>W: Processamento longo em andamento alt Processamento concluído W->>H: Encerra heartbeat W->>SQS: DeleteMessage SQS-->>W: Mensagem removida com sucesso else Worker falha ou é encerrado Note over H: Heartbeat para automaticamente Note over SQS: Timeout expira naturalmente Note over SQS: Mensagem reenviada para outro worker end
  1. Worker recebe a mensagem com Visibility Timeout inicial de, por exemplo, 60 segundos.
  2. Thread de heartbeat é iniciada em paralelo, configurada para chamar ChangeMessageVisibility a cada 45 segundos.
  3. Cada chamada de heartbeat reseta o timeout para mais 60 segundos a partir daquele momento.
  4. Processamento termina: thread principal chama DeleteMessage, thread de heartbeat é encerrada.
  5. 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 ApproximateNumberOfMessagesNotVisible crescendo 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

TermoDefinição
Visibility TimeoutPeríodo em que uma mensagem fica invisível para outros consumidores após ser recebida via ReceiveMessage
Receipt HandleIdentificador único retornado por cada ReceiveMessage, necessário para DeleteMessage e ChangeMessageVisibility. Muda a cada reentrega.
At-least-once DeliveryGarantia 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 IDAtributo de filas FIFO que garante ordenamento e processamento sequencial dentro de um grupo

Comentários

Postagens mais visitadas deste blog

EC2 SSH Connection Timeout: Quais Regras do Security Group Verificar

EC2 Sem Acesso à Internet em VPC Customizada: Como Configurar Internet Gateway e Route Table

S3 Access Denied: Por que 'Bloquear Acesso Público' impede seu objeto mesmo após torná-lo público