Filas & Jobs
9 min de leitura

Como implementar visibility timeout no SQS em tarefas demoradas: guia prático para produção

Aprenda a configurar, renovar e monitorar o visibility timeout no Amazon SQS para processar jobs longos sem reentregas perigosas, duplicidade e perda de trabalho.

M

Max Alex

Como implementar visibility timeout no SQS em tarefas demoradas: guia prático para produção
Neste artigo (9 tópicos)

O visibility timeout SQS é um dos controles mais importantes para executar tarefas demoradas com segurança no Amazon SQS. Quando ele é menor que a duração real de um job, a mesma mensagem pode reaparecer na fila e ser consumida por outro worker antes de a primeira execução terminar.

O resultado costuma ser caro: conversões duplicadas, chamadas repetidas a APIs, cobranças indevidas, concorrência inesperada e filas cada vez mais difíceis de operar. Neste guia, você verá como dimensionar o timeout inicial, renová-lo durante o trabalho e construir um fluxo pronto para produção.

O que é visibility timeout no Amazon SQS

Depois que um consumidor recebe uma mensagem com ReceiveMessage, o SQS a mantém temporariamente invisível para os demais consumidores. Esse intervalo é o visibility timeout.

Durante esse período, o worker deve processar o job e confirmar o sucesso com DeleteMessage. A exclusão é a confirmação real do processamento. Se ela não acontecer antes do prazo, a mensagem volta a ficar visível e pode ser entregue novamente.

Isso torna o SQS adequado para processamento assíncrono, mas não transforma a invisibilidade em um lock perfeito. Uma falha de processo, rede ou confirmação pode provocar uma nova entrega mesmo quando o primeiro worker já executou parte do trabalho.

Imagine uma mensagem que pede a geração de um relatório. Enquanto o worker trabalha, ela fica invisível. Ao terminar de persistir o arquivo e o status do relatório, o worker remove a mensagem. Se o worker cair antes disso, outro consumidor poderá retomá-la mais tarde.

Por que tarefas demoradas falham com o timeout padrão

O erro mais comum é definir um timeout curto e assumir que ele protege todo o processamento. Uma conversão de vídeo que dura 12 minutos, por exemplo, não estará protegida por um timeout de 30 segundos.

Quando os 30 segundos expiram, outro worker pode receber a mesma mensagem e começar a mesma conversão. As duas execuções passam a competir por CPU, arquivos, banco de dados e serviços externos. Em operações não idempotentes, o efeito pode chegar ao cliente.

É importante considerar que o SQS trabalha com entrega pelo menos uma vez. Portanto, operações idempotentes continuam necessárias, mesmo com um timeout bem calculado.

Também não basta aumentar indiscriminadamente o timeout da fila. Um prazo muito longo reduz a velocidade de recuperação quando um worker realmente falha: a mensagem só poderá ser reprocessada quando a invisibilidade terminar. O objetivo é equilibrar proteção contra duplicidade e recuperação de falhas.

Em sistemas distribuídos, mensagens duplicadas em filas devem ser tratadas como uma condição normal de operação, não como uma exceção impossível.

Como escolher o visibility timeout inicial

Baseie o intervalo na distribuição real da duração dos jobs, e não na média. A média esconde picos: se a maioria das tarefas termina em 20 segundos, mas uma parcela relevante leva 90 segundos, um timeout de 30 segundos ainda causará reentregas.

Uma regra prática é começar pelo p99 do processamento e adicionar margem para I/O, retries locais, persistência do resultado, encerramento do worker e a chamada de exclusão. Se o p99 é 90 segundos e a margem operacional é 20 segundos, comece com 120 segundos e acompanhe o comportamento em produção.

  • Jobs curtos e previsíveis: timeout próximo ao p99 com margem moderada.
  • Jobs longos e variáveis: timeout inicial conservador mais renovação periódica.
  • Jobs que podem ultrapassar horas: divida o trabalho em etapas quando possível e trate a renovação como requisito.

O SQS permite configurar visibility timeout de até 12 horas. Esse teto não deve ser a configuração padrão: ele é um limite técnico, não uma recomendação operacional.

Instrumente duração, falhas e reentregas para obter as métricas dos workers que sustentam esse cálculo. O timeout também deve respeitar os SLOs para processamento assíncrono: uma configuração que evita duplicidade, mas deixa o backlog crescer, ainda precisa ser revista.

Renove o timeout durante o processamento com ChangeMessageVisibility

Para tarefas cujo tempo é variável ou difícil de prever, use ChangeMessageVisibility como um heartbeat do processamento. A operação atualiza o tempo de invisibilidade da mensagem recebida usando o ReceiptHandle daquela entrega.

Renove antes de o prazo atual vencer. Um padrão simples é escolher um timeout de cinco minutos e tentar renová-lo a cada um ou dois minutos, com margem para falhas transitórias e latência de rede. Não espere os últimos segundos.

async function extendVisibility(sqs, queueUrl, receiptHandle) {
  await sqs.changeMessageVisibility({
    QueueUrl: queueUrl,
    ReceiptHandle: receiptHandle,
    VisibilityTimeout: 300
  });
}

Inicie a rotina de renovação após validar a mensagem e cancele-a imediatamente quando o processamento terminar, falhar de forma definitiva ou for cancelado. Se uma renovação falhar, registre o evento com o identificador do job e trate a mensagem como potencialmente reentregue; não presuma exclusividade.

Evite renovar apenas ao fim de uma etapa muito longa. Se cada etapa leva dois minutos, mas o timeout vence em cinco minutos, uma falha ou lentidão entre etapas pode criar uma janela de risco. Um timer independente e supervisionado costuma ser mais confiável.

Implemente um worker resiliente: receber, processar, renovar e excluir

O ciclo seguro começa antes do trabalho caro. Valide o schema e os campos obrigatórios do payload, gere ou propague um ID de correlação e registre o estado de execução em uma fonte durável quando o processo tiver efeitos relevantes.

  1. Receba a mensagem e capture o ReceiptHandle atual.
  2. Valide o payload e verifique se o job já foi concluído.
  3. Inicie a renovação periódica da invisibilidade.
  4. Execute o trabalho e persista o resultado ou estado final.
  5. Interrompa a renovação e chame DeleteMessage somente após o sucesso durável.
const renewal = setInterval(() => {
  extendVisibility(sqs, queueUrl, message.ReceiptHandle)
    .catch(error => logger.error({ error, jobId }, 'Falha ao renovar timeout'));
}, 60_000);

try {
  await processJob(payload, jobId);
  await saveCompletedState(jobId);
  await sqs.deleteMessage({
    QueueUrl: queueUrl,
    ReceiptHandle: message.ReceiptHandle
  });
} finally {
  clearInterval(renewal);
}

O exemplo mostra a ordem essencial, mas a implementação real precisa evitar que uma renovação continue após a exclusão e precisa observar falhas do timer. Em jobs críticos, mantenha o estado do trabalho separado da confirmação da mensagem. Assim, uma nova entrega consulta o estado persistido antes de repetir efeitos externos.

Nunca use o SQS como único mecanismo de deduplicação. O receipt handle identifica uma entrega, não uma garantia de que apenas um processo executará o efeito de negócio.

Projete para reentregas com idempotência e controle de concorrência

Mesmo com renovação correta, reentregas são possíveis. Um worker pode concluir uma chamada externa, perder conectividade antes do DeleteMessage e ter a mensagem entregue de novo. O projeto deve suportar esse cenário sem duplicar o resultado.

Use uma chave idempotente persistida, normalmente derivada do ID do job, pedido ou evento de origem. Antes de cobrar, enviar e-mail, criar recurso externo ou publicar uma alteração, verifique se aquela operação já foi concluída.

Uma abordagem comum é gravar uma linha de operação com unicidade no banco. O worker tenta criar ou reservar a operação; se encontrar um estado concluído, apenas confirma a mensagem. Se encontrar uma operação em andamento, aplica uma regra de retomada ou espera controlada.

Para efeitos externos, envie a mesma chave de idempotência ao provedor quando a API suportar esse mecanismo. Para recursos compartilhados, use transações, restrições únicas ou locks de negócio com expiração segura. Locks não substituem idempotência: eles reduzem concorrência, enquanto a idempotência protege o resultado.

Trate falhas com retries, DLQ e limites de recebimento

Falhas transitórias não devem levar à exclusão da mensagem. Se uma dependência estiver indisponível, deixe a mensagem retornar à fila ou ajuste sua invisibilidade para aplicar uma espera apropriada. Registre o motivo, a contagem de tentativas e o ID de correlação, sem expor dados sensíveis nos logs.

Configure uma dead letter queue SQS e uma política de redrive com maxReceiveCount compatível com o tipo de trabalho. Mensagens inválidas, esquemas incompatíveis e erros persistentes não devem circular indefinidamente na fila principal.

Escolha o limite considerando o custo e a possibilidade de recuperação. Um payload estruturalmente inválido pode ir para análise após poucas tentativas; uma falha temporária de serviço pode justificar mais tentativas, espaçadas por backoff.

Quando identificar uma falha definitiva, você pode reduzir a visibilidade para liberar a mensagem mais cedo, desde que isso seja coerente com a estratégia de retry. O ponto central é não mascarar uma mensagem problemática com renovações sem fim.

Monitore sinais de timeout inadequado em produção

Uma configuração saudável precisa ser comprovada por observabilidade. Acompanhe a idade aproximada da mensagem mais antiga, o volume de mensagens visíveis e em processamento, a profundidade da fila e a taxa de recebimentos repetidos.

Correlacione a duração de cada job com o número de renovações do visibility timeout. Crescimento inesperado nas extensões, mensagens recebidas muitas vezes ou falhas de DeleteMessage são sinais de que o timeout, a capacidade dos workers ou a lógica de processamento precisam de ajuste.

  • Alerta para idade da mensagem mais antiga acima do SLO.
  • Alerta para aumento de recebimentos repetidos e mensagens enviadas à DLQ.
  • Dashboard com duração por percentil, taxa de sucesso e capacidade disponível.
  • Logs estruturados com job ID, message ID, tentativa e resultado da renovação.

Teste cenários adversos antes de depender deles em incidente: mate um worker no meio do job, simule rede instável, atrase uma chamada externa e execute uma tarefa acima do p99. Verifique se a mensagem reaparece, se o estado é retomado e se nenhum efeito de negócio é duplicado.

Checklist de produção para visibility timeout no SQS

  • O timeout inicial foi definido a partir de percentis de duração e margem operacional.
  • Jobs longos renovam a invisibilidade antes do vencimento, usando o receipt handle atual.
  • O worker para de renovar após sucesso, cancelamento ou falha definitiva.
  • A mensagem só é excluída depois que o resultado está persistido de forma durável.
  • Efeitos externos possuem chave idempotente e controle de concorrência adequado.
  • A fila possui política de redrive, DLQ e procedimento para analisar mensagens isoladas.
  • Há alertas para backlog, idade de mensagens, reentregas e falhas de exclusão.
  • O time testou interrupção de worker e reprocessamento sem efeitos duplicados.

O visibility timeout SQS não elimina a necessidade de projetar para falhas; ele cria a janela de trabalho na qual um consumidor pode concluir sua tarefa com menos disputa. Em produção, a combinação correta é timeout dimensionado, renovação antecipada, idempotência, DLQ e métricas acionáveis.

Sua fila processa tarefas longas ou apresenta reentregas inesperadas? Estruture o fluxo com timeout dinâmico, idempotência e observabilidade antes que duplicidades afetem clientes e operações.

M
Publicado por

Max Alex

Desenvolvedor full stack especializado em SaaS, Laravel, IA aplicada, WhatsApp API, automações e integrações. Criação de sistemas web escaláveis para empresas.

Conhecer Max Alex
Próximas leituras

Continue lendo

Ver todos os artigos →