Loop Infinito entre Lambda e S3: Como Evitar o Gatilho Recursivo

Você configura uma função Lambda para processar arquivos enviados a um bucket S3 e salvar o resultado no mesmo bucket — parece razoável até o momento em que a função começa a se chamar recursivamente, consumindo invocações em escala exponencial e gerando uma conta inesperada no final do mês. Esse padrão de loop infinito entre Lambda e S3 é um dos erros operacionais mais comuns em arquiteturas orientadas a eventos na AWS.

TL;DR — Resumo do Problema e Soluções

SituaçãoCausaSolução Recomendada
Lambda salva no mesmo bucket que dispara o gatilhoNotificação S3 re-aciona a funçãoUsar bucket separado para saída
Bucket único é obrigatório por requisitoPrefixo de saída não filtradoFiltrar por prefixo de entrada no gatilho
Prefixo não resolve (ex: transformação no mesmo prefixo)Filtro insuficienteUsar tag de metadado no objeto ou verificação lógica no código
Loop já em execuçãoInvocações acumuladas na filaDesabilitar o gatilho imediatamente via console ou CLI

Como o Loop Infinito com Lambda e S3 Acontece

O Amazon S3 suporta notificações de eventos que disparam uma função Lambda quando um objeto é criado no bucket. O problema surge quando a própria função Lambda grava um objeto no mesmo bucket — o S3 interpreta essa gravação como um novo evento e aciona a função novamente. Sem nenhum mecanismo de parada, o ciclo se repete indefinidamente.

graph LR Upload(["Upload Inicial
objeto no bucket"]) --> S3Evt["S3 Emite Evento
ObjectCreated"] S3Evt --> Lambda["Lambda Executa
processa arquivo"] Lambda --> Grava["Grava resultado
no mesmo bucket"] Grava --> S3Evt2["S3 Emite Evento
ObjectCreated novamente"] S3Evt2 --> Lambda2["Lambda Executa
outra vez"] Lambda2 --> Grava2["Grava resultado
no mesmo bucket"] Grava2 --> Loop(["... loop infinito"]) style Loop fill:#ff4444,color:#fff style S3Evt2 fill:#ff8800,color:#fff style Lambda2 fill:#ff8800,color:#fff
  1. Upload inicial: um objeto chega ao bucket S3 (ex: prefixo input/).
  2. Notificação S3: o bucket emite um evento s3:ObjectCreated:* e aciona a função Lambda.
  3. Processamento: a Lambda lê o arquivo, processa e grava o resultado — no mesmo bucket, sem prefixo diferenciado.
  4. Re-disparo: a gravação do resultado gera um novo evento ObjectCreated, acionando a Lambda outra vez.
  5. Loop: o ciclo continua até estourar a cota de concorrência, o limite de conta, ou gerar um custo significativo.
Pense no gatilho S3 como um sensor de movimento na porta de um quarto: se você instala o sensor dentro do quarto e a porta abre para dentro, qualquer movimento dentro do quarto também aciona o sensor — incluindo o movimento causado pela própria abertura da porta.

Solução 1: Buckets Separados para Entrada e Saída (Recomendado)

A abordagem mais limpa e operacionalmente segura é usar dois buckets distintos: um para receber os arquivos de entrada (onde o gatilho está configurado) e outro para armazenar os resultados processados. A Lambda lê do bucket de entrada e escreve no bucket de saída — o gatilho nunca é acionado pela escrita.

Criando os Buckets

# Bucket de entrada — gatilho configurado aqui
aws s3api create-bucket \
  --bucket meu-bucket-entrada \
  --region us-east-1

# Bucket de saída — Lambda escreve aqui, sem gatilho
aws s3api create-bucket \
  --bucket meu-bucket-processados \
  --region us-east-1

Note que para us-east-1 (us-east-1 é a região padrão da AWS), o parâmetro --create-bucket-configuration não deve ser utilizado — a AWS retorna erro se você incluir LocationConstraint para essa região específica.

Configurando o Gatilho Apenas no Bucket de Entrada

aws lambda add-permission \
  --function-name minha-funcao-processamento \
  --statement-id s3-trigger-entrada \
  --action lambda:InvokeFunction \
  --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::meu-bucket-entrada \
  --source-account 123456789012
aws s3api put-bucket-notification-configuration \
  --bucket meu-bucket-entrada \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:minha-funcao-processamento",
        "Events": ["s3:ObjectCreated:*"]
      }
    ]
  }'

Política IAM Mínima para a Função Lambda

🔽 Clique para expandir a política IAM
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LerDosBucketEntrada",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::meu-bucket-entrada/*"
    },
    {
      "Sid": "EscreverNoBucketSaida",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::meu-bucket-processados/*"
    }
  ]
}

Restringir a permissão s3:PutObject apenas ao bucket de saída na política IAM adiciona uma camada de proteção: mesmo que o código da função contenha um bug de path, ela não conseguirá escrever no bucket de entrada.

Solução 2: Filtro por Prefixo no Mesmo Bucket

Quando o uso de um único bucket é um requisito não negociável, a segunda opção é configurar o gatilho S3 com filtros de prefixo e sufixo, de forma que apenas objetos no prefixo de entrada acionem a Lambda — e a função grave os resultados em um prefixo diferente, fora do escopo do filtro.

graph LR Upload(["Upload: input/arquivo.csv"]) --> Filtro{"Filtro do Gatilho
prefixo: input/"} Filtro -->|"corresponde"| Lambda["Lambda Executa"] Lambda --> Saida["Grava: output/arquivo.csv"] Saida --> Filtro2{"Filtro do Gatilho
prefixo: input/"} Filtro2 -->|"não corresponde
— ignorado"| Stop(["Nenhuma invocação"]) style Stop fill:#22aa44,color:#fff style Filtro2 fill:#2255cc,color:#fff
  1. O gatilho escuta apenas objetos criados sob o prefixo input/.
  2. A Lambda processa e grava o resultado sob o prefixo output/.
  3. A criação do objeto em output/ não corresponde ao filtro do gatilho — nenhuma nova invocação ocorre.

Configurando o Gatilho com Filtro de Prefixo

aws s3api put-bucket-notification-configuration \
  --bucket meu-bucket-unico \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:minha-funcao-processamento",
        "Events": ["s3:ObjectCreated:*"],
        "Filter": {
          "Key": {
            "FilterRules": [
              {
                "Name": "prefix",
                "Value": "input/"
              }
            ]
          }
        }
      }
    ]
  }'

Um detalhe crítico: o S3 não permite configurar duas notificações com filtros de prefixo que se sobreponham para o mesmo destino. Se você já tiver outra notificação configurada no bucket, verifique se os prefixos são mutuamente exclusivos antes de aplicar a configuração acima.

Solução 3: Verificação Lógica no Código da Função

Esta abordagem funciona como uma camada de defesa adicional, não como solução primária. A ideia é inspecionar os metadados do objeto recebido no evento — como o prefixo da chave ou um metadado customizado — e encerrar a execução imediatamente se o arquivo já foi processado.

import boto3

s3 = boto3.client('s3')

def lambda_handler(event, context):
    record = event['Records'][0]
    bucket = record['s3']['bucket']['name']
    key = record['s3']['object']['key']

    # Encerra se o objeto já está no prefixo de saída
    if key.startswith('output/'):
        print(f'Objeto {key} ignorado — prefixo de saída detectado.')
        return

    # Processamento real aqui
    response = s3.get_object(Bucket=bucket, Key=key)
    conteudo = response['Body'].read()

    # Salva resultado com prefixo diferente
    chave_saida = key.replace('input/', 'output/', 1)
    s3.put_object(Bucket=bucket, Key=chave_saida, Body=conteudo)
    print(f'Processado: {key} -> {chave_saida}')

Essa verificação no código é útil como proteção secundária, mas não substitui o filtro no gatilho. Se o filtro falhar por uma reconfiguração acidental, a lógica no código ainda evita o loop.

Como Parar um Loop que Já Está em Execução

Se o loop já está ativo, a prioridade é interromper as invocações antes de investigar a causa. A forma mais rápida é desabilitar o mapeamento de origem de eventos (event source mapping) ou remover a permissão do gatilho S3.

Passo 1 — Identificar e Desabilitar o Gatilho

# Listar as configurações de notificação do bucket
aws s3api get-bucket-notification-configuration \
  --bucket meu-bucket-problema
# Remover todas as notificações do bucket (para emergência)
aws s3api put-bucket-notification-configuration \
  --bucket meu-bucket-problema \
  --notification-configuration '{}'

Passo 2 — Verificar Invocações em Andamento

# Verificar a concorrência atual da função
aws lambda get-function-concurrency \
  --function-name minha-funcao-processamento
# Definir concorrência reservada como 0 para bloquear novas invocações
aws lambda put-function-concurrency \
  --function-name minha-funcao-processamento \
  --reserved-concurrent-executions 0

Definir a concorrência reservada como 0 efetivamente throttle todas as invocações da função — elas retornarão erro TooManyRequestsException em vez de executar. Isso dá tempo para limpar os objetos duplicados no bucket e reconfigurar o gatilho corretamente antes de restaurar a concorrência.

Passo 3 — Restaurar a Concorrência Após Correção

# Remover a restrição de concorrência reservada
aws lambda delete-function-concurrency \
  --function-name minha-funcao-processamento

Diagnóstico: Sintoma, Diagnóstico Errado e Causa Real

O alerta chega via CloudWatch: a métrica Invocations da função está subindo de forma exponencial, e o custo do Lambda disparou em minutos. O primeiro instinto é verificar se há algum cliente externo chamando a função em loop — você olha os logs do API Gateway, não encontra nada suspeito, e perde tempo investigando a camada errada.

A causa real está nos logs do CloudWatch da própria função: cada invocação registra um key diferente, mas todos seguem o padrão processed-processed-processed-arquivo.csv — o prefixo 'processed-' sendo concatenado a cada ciclo. A função estava renomeando o arquivo com um prefixo fixo, mas o gatilho estava configurado sem filtro de prefixo, então cada arquivo renomeado acionava uma nova invocação.

# Verificar os logs recentes para identificar o padrão das chaves
aws logs filter-log-events \
  --log-group-name /aws/lambda/minha-funcao-processamento \
  --start-time $(date -d '10 minutes ago' +%s000) \
  --filter-pattern '"key"' \
  --query 'events[*].message' \
  --output text

Se os logs mostrarem chaves com prefixos repetidos ou crescentes, o loop está confirmado. A correção é aplicar o filtro de prefixo no gatilho ou migrar para buckets separados.

stateDiagram-v2 [*] --> LoopAtivo : Invocações exponenciais detectadas LoopAtivo --> GatilhoDesabilitado : Remover notificação S3 GatilhoDesabilitado --> ConcorrenciaZero : Definir concorrência reservada = 0 ConcorrenciaZero --> Investigacao : Analisar logs CloudWatch Investigacao --> CorrecaoAplicada : Separar buckets ou adicionar filtro CorrecaoAplicada --> ConcorrenciaRestaurada : Remover restrição de concorrência ConcorrenciaRestaurada --> GatilhoReconfigurado : Reconfigurar notificação com filtro GatilhoReconfigurado --> [*] : Operação normal retomada

Prevenção com Recursive Loop Detection (Proteção Nativa AWS)

A partir de 2023, a AWS introduziu detecção de loop recursivo para Lambda. Quando a AWS detecta que uma função Lambda está sendo invocada recursivamente por um serviço suportado (incluindo S3 via SQS ou SNS como intermediário), ela pode interromper automaticamente as invocações e notificar via CloudWatch. Essa proteção opera como uma rede de segurança — não substitui a arquitetura correta, mas reduz o impacto de um loop acidental.

Para verificar se a proteção está habilitada na sua conta:

aws lambda get-account-settings

Consulte a documentação oficial da AWS sobre 'Recursive loop detection' para verificar quais integrações são cobertas e os comportamentos exatos de interrupção, pois o escopo da proteção pode evoluir com o tempo.

Próximos Passos e Conclusão — Prevenindo o Loop Infinito entre Lambda e S3

O padrão mais seguro para evitar o loop infinito entre Lambda e S3 é sempre separar os buckets de entrada e saída. Filtros de prefixo funcionam, mas dependem de disciplina operacional contínua — qualquer alteração no código que mude o prefixo de saída pode reativar o loop silenciosamente.

  • Use buckets separados como padrão arquitetural padrão.
  • Adicione verificação de prefixo no código como defesa em profundidade.
  • Configure alarmes CloudWatch na métrica Invocations com threshold anômalo para detecção precoce.
  • Documente o prefixo de saída esperado como parte da configuração do gatilho.

Referências oficiais:

Glossário

TermoDefinição
Event Source MappingConfiguração que conecta uma fonte de eventos (como S3 ou SQS) a uma função Lambda, definindo quando a função será invocada.
s3:ObjectCreated:*Tipo de evento S3 que cobre qualquer operação de criação de objeto: PUT, POST, COPY e multipart upload completo.
Concorrência ReservadaLimite máximo de execuções simultâneas alocado exclusivamente para uma função Lambda específica. Definir como 0 bloqueia todas as invocações.
Filtro de PrefixoRegra na configuração de notificação S3 que restringe o disparo de eventos apenas a objetos cujas chaves começam com um determinado string.
Recursive Loop DetectionProteção nativa da AWS que detecta e interrompe invocações Lambda em loop recursivo em integrações suportadas.

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