O blog da AWS

Melhores práticas para funções duráveis do Lambda usando um exemplo de detecção de fraude

Por Debasis Rath, Arquiteto de Soluções Sênior na Amazon Web Services e Joe Losinski, Arquiteto de Soluções na Amazon Web Services. 

As funções duráveis do AWS Lambda estendem o modelo de programação do Lambda para criar aplicações tolerantes a falhas de múltiplas etapas e fluxos de trabalho de IA usando linguagens de programação familiares. Elas preservam o progresso apesar de interrupções e a execução pode ser suspensa por até um ano, para aprovações humanas, atrasos programados ou outros eventos externos, sem gerar cobranças de computação para funções sob demanda.

Esta publicação apresenta um sistema de detecção de fraude construído com funções duráveis, e também destaca as melhores práticas que você pode aplicar aos seus próprios fluxos de trabalho de produção, desde processos de aprovação até pipelines de dados e orquestração de agentes de IA. Você aprenderá como lidar com notificações simultâneas, aguardar respostas de clientes e recuperar-se de falhas sem perder o progresso. Se você ainda não conhece funções duráveis, confira primeiro a publicação Introdução às Funções Duráveis.

Detecção de fraude com humano no circuito

Considere um sistema de detecção de fraude de cartão de crédito que usa um agente de IA para analisar transações recebidas e atribuir pontuações de risco. Para casos ambíguos (pontuações de risco médio), o sistema precisa de aprovação humana antes de autorizar uma transação. O fluxo de trabalho se divide com base no risco:

  • Baixo risco (pontuação < 3): Autorizar imediatamente
  • Alto risco (pontuação ≥ 5): Enviar para o departamento de fraude imediatamente
  • Risco médio (pontuação 3–4): Suspender transação, enviar SMS e e-mail para o titular do cartão, aguardar até 24 horas pela confirmação (o tempo de espera é configurável)

Figura 1. Detecção de Fraude Agêntica com funções duráveis do Lambda

Com fluxos de trabalho com humano no circuito, os tempos de resposta podem variar de minutos a horas. Esses atrasos introduzem a necessidade de preservar o estado de forma durável sem consumir recursos de computação durante a espera. Com sistemas financeiros, também devemos implementar idempotência para nos proteger contra mensagens duplicadas (invocações) e nos recuperar de falhas sem reprocessar trabalho concluído. Para atender a esses requisitos, os desenvolvedores implementam padrões de polling com armazenamentos de estado externos como Amazon DynamoDB ou Amazon Simple Storage Service (Amazon S3) para gerenciar idempotência, pagam por computação ociosa enquanto aguardam callbacks, introduzem componentes de orquestração externos ou constroem sistemas assíncronos orientados a mensagens para lidar com tarefas de longa duração.

As funções duráveis do Lambda oferecem uma nova alternativa para enfrentar esses desafios por meio da execução durável, um padrão que usa pontos de verificação (snapshots de estado salvos) para preservar o progresso e reproduz a partir do estado salvo para se recuperar de falhas ou retomar após uma espera. Com capacidades de ponto de verificação, você não precisa mais pagar por computação do Lambda enquanto aguarda, seja por callbacks, atrasos programados ou eventos externos. Saiba como implementar funções duráveis usando a implementação completa de detecção de fraude neste repositório do GitHub. Você pode implantá-lo na sua conta da AWS e experimentar o código enquanto lê. O repositório inclui instruções de implantação, dados de exemplo e funções auxiliares para testes.

Ao percorrer o código, vamos focar nas melhores práticas para projetar fluxos de trabalho com execução durável e como aplicar esses padrões corretamente em fluxos de trabalho de produção.

Projete etapas idempotentes

A execução durável é projetada para preservar o progresso através de pontos de verificação e reprodução, mas esse modelo de confiabilidade significa que a lógica das etapas pode ser executada mais de uma vez. Quando as etapas são reexecutadas, como prevenir ações duplicadas como cobranças no cartão de crédito ou notificações repetidas de SMS ou e-mail ao cliente?

As funções duráveis usam execução pelo menos uma vez por padrão, executando cada etapa pelo menos uma vez, potencialmente mais se ocorrerem falhas. Quando uma etapa falha, ela é reexecutada. Existem duas estratégias para projetar etapas idempotentes que previnem efeitos colaterais duplicados: usando chaves de idempotência de API externa e usando a semântica de etapa no máximo uma vez, incorporada nas funções duráveis.

Estratégia A: Chaves de Idempotência de API Externa

// Strategy A: Use external API idempotency keys
await context.step(`authorize-${tx.id}`, async () => {
  return payment.charges.create({
    amount: tx.amount,
    currency: 'usd',
    idempotency_key: `tx-${tx.id}`, // Prevents duplicate charges
    description: `Transaction ${tx.id}`
  });
});
Code

Observe a configuração:

  • idempotency_key na chamada da API: Se a etapa for reexecutada, o processador de pagamento reconhece que é uma solicitação duplicada e retorna o resultado original
  • Defesa em profundidade: Duas camadas de proteção: ponto de verificação do Lambda e idempotência de API externa

Cada camada oferece proteção independente. Se o ponto de verificação do Lambda falhar, a API externa evita cobranças duplicadas. Para sistemas legados sem suporte a idempotência, onde é crítico que uma operação não seja executada mais de uma vez, use a semântica no máximo uma vez:

Estratégia B: Use Semântica No Máximo Uma Vez

Para sistemas legados sem suporte a idempotência, use execução no máximo uma vez, um recurso que executa cada etapa zero ou uma vez, nunca mais:

// Strategy B: At-most-once step semantics
await context.step("charge-legacy-system", async () => {
  return await legacyPaymentSystem.charge(tx.amount);
}, {
  semantics: StepSemantics.AtMostOncePerRetry,
  retryStrategy: createRetryStrategy({ maxAttempts: 0 })
});
Code

Isso cria pontos de verificação antes da execução da etapa, evitando a reexecução em novas tentativas. O trade-off? Se a etapa falhar, você deve decidir se tenta novamente (arriscando duplicatas) ou falha todo o fluxo de trabalho.

Use idempotência para efeitos colaterais críticos, como processamento de pagamento, gravações em banco de dados, chamadas de API externa, transições de estado e provisionamento de recursos. Saiba mais sobre idempotência na documentação.

Previna execuções duplicadas com DurableExecutionName

Etapas idempotentes evitam efeitos colaterais duplicados dentro de uma única execução, mas e quanto a execuções de fluxo de trabalho duplicadas rodando simultaneamente? Por exemplo, mensagens duplicadas na fila, usuários clicando “Enviar” várias vezes na interface do usuário, ou o mesmo evento chegando por múltiplos canais como webhook e API. Sem essa proteção, cada invocação cria uma execução durável separada, potencialmente executando a verificação de fraude múltiplas vezes, enviando notificações duplicadas e gerando confusão sobre qual execução é a autoritativa. As funções duráveis oferecem DurableExecutionName para ajudar a garantir apenas uma execução simultânea por nome único.

// Invoke fraud detection function with execution name
await lambda.invoke({
  FunctionName: 'fraud-detection',
  InvocationType: 'Event',
  DurableExecutionName: `tx-${transactionId}`,
  Payload: JSON.stringify({
    id: transactionId,
    amount: 6500,
    location: 'New York, NY',
    vendor: 'Amazon.com'
  })
});
Code

Observe a configuração:

  • DurableExecutionName: tx-${transactionId}: Usa o ID da transação como um identificador de execução único
  • InvocationType: ‘Event’: Invocação assíncrona suporta fluxos de trabalho de longa duração além de 15 minutos
  • Uma execução por transação: Se três invocações chegarem com o mesmo ID de transação, apenas a primeira cria uma execução. Solicitações subsequentes com o mesmo nome de execução e payload recebem uma resposta idempotente retornando o ARN da execução existente, em vez de criar uma nova execução.

As funções duráveis do Lambda funcionam com fontes de eventos do Lambda, incluindo mapeamentos de fonte de eventos (ESM) como Amazon Simple Queue Service (Amazon SQS)Amazon Kinesis e DynamoDB Streams. ESMs invocam funções duráveis de forma síncrona e herdam o limite de invocação de 15 minutos do Lambda. Portanto, assim como invocações diretas Request/Response, execuções de funções duráveis usando mapeamentos de fonte de eventos não podem exceder 15 minutos.

Para fluxos de trabalho que excedem 15 minutos, use uma função Lambda intermediária entre o mapeamento de fonte de eventos e a função durável:

// Intermediary function for SQS -> Durable function
export const handler = async (event) => {
  for (const record of event.Records) {
    const transaction = JSON.parse(record.body);
    await lambda.invoke({
      FunctionName: process.env.FRAUD_DETECTION_FUNCTION,
      InvocationType: 'Event',
      DurableExecutionName: `tx-${transaction.id}`,
      Payload: JSON.stringify(transaction)
    });
  }
};
Code

Isso remove o limite de 15 minutos, permite execuções de até um ano e ativa parâmetros personalizados de nome de execução para idempotência. Use Powertools for AWS Lambda para evitar invocações duplicadas da função durável quando o mapeamento de fonte de eventos reexecuta a função intermediária. Além disso, configure o tratamento de falhas da sua fonte de eventos para capturar invocações com falha para redrive ou replay futuro. Por exemplo, filas de mensagens mortas para SQS ou destinos de falha para outras fontes de eventos.

Combine timeouts com o tipo de invocação

Um detalhe de configuração importante conecta esses padrões: a combinação das configurações de timeout com o tipo de invocação. Invocações síncronas do Lambda (RequestResponse) têm um limite rígido de timeout de 15 minutos. Se você configurar uma execução durável para rodar por 24 horas, mas invocá-la de forma síncrona, a invocação síncrona falha com uma exceção. As funções duráveis suportam fluxos de trabalho de até um ano quando invocadas de forma assíncrona.

// Lambda function configuration
{
  FunctionName: 'fraud-detection',
  Timeout: 300,
  MemorySize: 512,
  DurableConfig: {
    ExecutionTimeout: 90000
  }
}
Code

E invoque de forma assíncrona:

// Async invocation for long-running workflow
await lambda.invoke({
  FunctionName: 'fraud-detection',
  InvocationType: 'Event',
  DurableExecutionName: `tx-${transactionId}`,
  Payload: JSON.stringify(transaction)
});
Code

Observe a configuração:

  • Timeout: 300: Timeout da função Lambda (5 minutos neste exemplo, até um máximo de 15 minutos). Isso define a duração máxima de cada fase de execução ativa, incluindo a invocação inicial e quaisquer reproduções subsequentes. Configure esse valor para cobrir o tempo de processamento ativo mais longo esperado no seu fluxo de trabalho.
  • ExecutionTimeout: { hours: 25 }: O timeout de execução durável cobre a duração total esperada do fluxo de trabalho, incluindo períodos de suspensão. Configure esse valor ligeiramente acima do timeout de espera mais longo para evitar casos extremos.
  • InvocationType: ‘Event’: A invocação assíncrona remove o limite de 15 minutos e permite execuções de até um ano.

O timeout da função Lambda se aplica às fases de execução ativa (chamadas de IA, envio de notificação). Durante a suspensão (aguardando callbacks), a função não está em execução, então esse timeout não se aplica. Configurar o timeout de execução durável com um limite significativo evita que fluxos de trabalho rodem mais tempo do que o esperado. Sem um timeout explícito, as execuções podem rodar até o tempo de vida máximo de um ano.

 

Síncrona (RequestResponse) Assíncrona (Event)
Duração total Menos de 15 minutos Até 1 ano
Chamador precisa do resultado Sim Não
Suporte a idempotência Sim Sim
Esperas com suspensão Sim Sim

Execute Operações Concorrentes com context.parallel()

No fluxo de trabalho de detecção de fraude, o sistema notifica o titular do cartão através de múltiplos canais como SMS e e-mail. Preservar a lógica de negócio ao executar fluxos de trabalho paralelos introduz complexidades de código, como gerenciar o estado de execução entre ramificações, lidar com sincronização e coordenar a conclusão das ramificações. As funções duráveis simplificam a implementação de fluxo de trabalho paralelo usando context.parallel(), que executa ramificações simultaneamente enquanto mantém pontos de verificação duráveis para cada ramificação e oferece opções configuráveis para lidar com conclusões parciais. Ao criar pontos de verificação e gerenciar o estado internamente, as funções duráveis ajudam a garantir que o estado seja preservado mesmo em caso de novas tentativas ou falhas. Observe que context.parallel() gerencia o estado de execução interno de cada ramificação. Se suas ramificações interagem com um estado externo compartilhado (como um banco de dados), você é responsável por gerenciar o acesso concorrente a esse estado.

// Human-in-the-loop: verify via email AND SMS (first response wins)
let verified = await context.parallel("human-verification", [
  (ctx) => ctx.waitForCallback("SendVerificationEmail",
    async (callbackId) => sendCustomerNotification(callbackId, 'email', tx)
  ),
  (ctx) => ctx.waitForCallback("SendVerificationSMS",
    async (callbackId) => sendCustomerNotification(callbackId, 'sms', tx)
  )
], {
  maxConcurrency: 2,
  completionConfig: {
    minSuccessful: 1 // Continue after 1 success
  }
});
Code

Observe a configuração:

  • maxConcurrency: 2: Ambas as notificações enviadas ao mesmo tempo
  • minSuccessful: 1: Precisamos que apenas um canal tenha sucesso — o que responder primeiro vence

Cada ramificação paralela aguarda seu callback de forma independente, e a execução durável cria pontos de verificação para cada ramificação como parte do estado de execução. Usando o parâmetro minSuccessful, você controla o número mínimo de ramificações bem-sucedidas necessárias para que a operação paralela seja concluída. Neste exemplo, apenas uma das duas ramificações precisa ter sucesso. Verificações por SMS ou e-mail são ambas válidas, e o fluxo de trabalho é retomado assim que qualquer canal é concluído com sucesso. Chamamos isso de padrão primeira-resposta-vence. Este padrão funciona bem quando você precisa de apenas um resultado bem-sucedido de qualquer ramificação paralela e quer que as ramificações restantes deixem de bloquear o progresso.

Mas o que acontece se nenhum canal responder? Sem timeouts, este fluxo de trabalho poderia permanecer suspenso até o tempo de vida de execução configurado.

Sempre configure timeouts de callback

Vamos adicionar proteção de timeout à verificação paralela da seção anterior. O context.waitForCallback() aceita uma opção de timeout que limita por quanto tempo cada ramificação aguarda antes de lançar uma exceção. Ao envolver a chamada paralela em um try/catch, você pode implementar lógica de fallback quando os usuários não respondem a tempo.

// Enhanced: parallel verification with timeout and error handling
let verified;
try {
  verified = await context.parallel("human-verification", [
    (ctx) => ctx.waitForCallback("SendVerificationEmail",
      async (callbackId) => sendCustomerNotification(callbackId, 'email', tx),
      { timeout: { days: 1 } }  // Wait up to 1 day for email response
    ),
    (ctx) => ctx.waitForCallback("SendVerificationSMS",
      async (callbackId) => sendCustomerNotification(callbackId, 'sms', tx),
      { timeout: { days: 1 } }  // Wait up to 1 day for SMS response
    )
  ], {
    maxConcurrency: 2,
    completionConfig: {
      minSuccessful: 1
    }
  });
} catch (error) {
  const isTimeout = error.message?.includes("timeout");
  if (isTimeout) {
    context.logger.warn("Customer verification timeout", { error, txId: tx.id });
    // Fallback: escalate to fraud department
    return await context.step("sendToFraudDepartment", async () =>
      sendToFraudDepartment(tx, true)
    );
  }
  throw error; // Re-throw non-timeout errors
}
Code

Observe o que mudou da seção anterior:

  • timeout: { days: 1 }: Cada ramificação de callback agora tem um tempo máximo de espera de 1 dia. Se nem o callback de e-mail nem o de SMS chegarem dentro dessa janela, uma exceção de timeout é lançada.
  • try/catch com detecção de timeout: O bloco catch diferencia erros de timeout de outras exceções. Quando um timeout ocorre, o fluxo de trabalho implementa lógica de fallback escalando a transação para o departamento de fraude, enquanto erros não relacionados a timeout são relançados para tratamento pelo mecanismo de nova tentativa da execução durável.

Sem esse tratamento de erro, toda a execução falha de forma não controlada. O timeout também funciona com o minSuccessful:configuration se uma ramificação expira, mas a outra tem sucesso, a operação paralela ainda é concluída com sucesso, já que apenas um resultado bem-sucedido é necessário.

Para casos de uso avançados em que o tratador de callback realiza trabalho de longa duração, você também pode configurar um heartbeatTimeout para detectar callbacks paralisados antes que o timeout principal expire. Veja o Guia do Desenvolvedor do Lambda para detalhes.

Use timeouts de callback para aprovações humanas, callbacks de API externa, processamento assíncrono e integrações de terceiros.

Juntando tudo: implementação completa de detecção de fraude

Agora vamos ver como todas as melhores práticas se integram no fluxo de trabalho completo de detecção de fraude:

import { withDurableExecution } from "@aws/durable-execution-sdk-js";
import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore";

const agentRuntimeArn = process.env.AGENT_RUNTIME_ARN;
const agentRegion = process.env.AGENT_REGION || 'us-east-1';
const client = new BedrockAgentCoreClient({ region: agentRegion });

export const handler = withDurableExecution(async (event, context) => {
  const tx = {
    id: event.id,
    amount: event.amount,
    location: event.location,
    vendor: event.vendor
  };

  // AI fraud assessment with error handling
  tx.score = await context.step("fraudCheck", async () => {
    try {
      const payloadJson = JSON.stringify({ input: { amount: tx.amount } });
      const command = new InvokeAgentRuntimeCommand({
        agentRuntimeArn: agentRuntimeArn,
        qualifier: 'DEFAULT',
        payload: Buffer.from(payloadJson, 'utf-8'),
        contentType: 'application/json',
        accept: 'application/json'
      });
      const response = await client.send(command);
      const responseText = await response.response.transformToString();
      const result = JSON.parse(responseText);
      return result?.output?.risk_score ?? 5;  // Default to high-risk if score unavailable
    } catch (error) {
      context.logger.error("Fraud check failed", { error, txId: tx.id });
      return 5;
    }
  });

  // Route based on AI decision
  if (tx.score < 3) {
    // Best Practice: Idempotent authorization
    return await context.step(`authorize-${tx.id}`, async () =>
    authorizeTransaction(tx, { idempotency_key: `tx-${tx.id}` })
    );
  }

  if (tx.score >= 5) {
    return await context.step(`sendToFraudDepartment-${tx.id}`, async () =>
      sendToFraudDepartment(tx)
    );
  }

  // Medium risk: need human verification
  await context.step(`suspend-${tx.id}`, async () => suspendTransaction(tx));

  // Best Practice: Concurrent operations with timeout configuration
  let verified;
  try {
    verified = await context.parallel("human-verification", [
      (ctx) => ctx.waitForCallback("SendVerificationEmail",
        async (callbackId) => sendCustomerNotification(callbackId, 'email', tx),
        { timeout: { days: 1 } }
      ),
      (ctx) => ctx.waitForCallback("SendVerificationSMS",
        async (callbackId) => sendCustomerNotification(callbackId, 'sms', tx),
        { timeout: { days: 1 } }
      )
    ], {
      maxConcurrency: 2,
      completionConfig: {
        minSuccessful: 1
      }
    });
  } catch (error) {
    const isTimeout = error.message?.includes("timeout");
    context.logger.warn(
      isTimeout ? "Customer verification timeout" : "Customer verification failed",
      { error, txId: tx.id }
    );
    return await context.step(`timeout-escalate-${tx.id}`, async () =>
      sendToFraudDepartment(tx, true)
    );
  }

  // Idempotent final step with idempotency key
  return await context.step(`finalize-${tx.id}`, async () => {
    const action = !verified.hasFailure && verified.successCount > 0
      ? "authorize"
      : "escalate";
    if (action === "authorize") {
      return authorizeTransaction(tx, true, { idempotency_key: `finalize-${tx.id}` });
    }
    return sendToFraudDepartment(tx, true);
  });
});
Code

Veja como as melhores práticas se integram: context.parallel() envia SMS e e-mail simultaneamente, retomando quando qualquer canal responder. Ambos os callbacks configuram timeouts de 1 dia, com tratamento try/catch que escala em caso de timeout. O parâmetro DurableExecutionName: tx-${transactionId} (especificado no momento da invocação, mostrado no exemplo de CLI a seguir) oferece deduplicação no nível de execução, enquanto chaves de idempotência nas etapas de autorização evitam cobranças duplicadas na camada de aplicação. A invocação assíncrona (InvocationType: 'Event') viabiliza o período de espera de 24 horas.

Após a implantação, invoque a função de forma assíncrona com uma transação de exemplo para verificar o resultado:

transactionId="123456789"
aws lambda invoke \
  --function-name "fraud-detection:$LATEST" \
  --invocation-type Event \
  --durable-execution-name "tx-${transactionId}" \
  --cli-binary-format raw-in-base64-out \
  --payload "{\"id\": \"${transactionId} \", \"amount\": 6500, \"location\": \"New York, NY\", \"vendor\": \"Amazon.com\"}" \
  --region us-east-2 \
  response.json
Code

Após uma invocação bem-sucedida, você pode visualizar o estado de execução na seção de operações duráveis do console do Lambda. A execução mostra um estado suspenso, aguardando a resposta do cliente:

Figura 2: Estado de execução suspenso

Observe que as etapas fraudCheck e suspendTransaction aparecem como bem-sucedidas, com resultados em pontos de verificação. A operação paralela human-verification mostra que ambas as ramificações SMS e e-mail iniciaram. A linha do tempo mostra a função em estado suspenso. Simule uma resposta do cliente enviando um callback de sucesso por meio do console, AWS Command Line Interface (AWS CLI) ou API do Lambda:

aws lambda send-durable-execution-callback-success \
	--callback-id <CALLBACK_ID_FROM_EMAIL_OR_SMS> \
	--result '{"status":"approved","channel":"email"}' \
	--cli-binary-format raw-in-base64-out
Code

Figura 3: Execução concluída com aprovação do cliente

Após receber a aprovação do cliente, a execução durável é retomada a partir do seu ponto de verificação, autoriza a transação e é concluída. A execução durou horas, mas consumiu apenas segundos de computação.

Conclusão

Com funções duráveis, o Lambda vai além do processamento de evento único para impulsionar processos de negócio centrais e fluxos de trabalho de longa duração, mantendo a simplicidade operacional, a confiabilidade e a escala que definem o Lambda. Você pode criar aplicações que rodam por dias ou meses, sobrevivem a falhas e retomam de onde pararam, tudo dentro do familiar modelo de programação orientado a eventos.

Implante o fluxo de trabalho de detecção de fraude do nosso repositório do GitHub e experimente padrões de humano no circuito na sua própria conta. Para conceitos centrais, veja Introdução às Funções Duráveis do AWS Lambda. Para documentação abrangente, veja o Guia do Desenvolvedor do Lambda. Explore o Serverless Land para arquiteturas de referência e descubra onde a execução durável se encaixa nos seus projetos.

Compartilhe feedback, perguntas e casos de uso nos repositórios do SDK ou no re:Post.

Este conteúdo é uma tradução da publicação original em inglês, que pode ser encontrada aqui.

Biografia do Autores

Debasis Rath é um Arquiteto de Soluções Sênior na Amazon Web Services
Joe Losinski é um Arquiteto de Soluções na Amazon Web Services. 

Biografia do tradutores

Daniel Abib é Arquiteto de Soluções Sênior e Especialista em Amazon Bedrock na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e especialização em Machine Learning. Ele trabalha apoiando Startups, ajudando-os em sua jornada para a nuvem.

https://www.linkedin.com/in/danielabib/

Rodrigo Peres é Arquiteto de Soluções na AWS, com mais de 20 anos de experiência trabalhando com arquitetura de soluções, desenvolvimento de sistemas e modernização de sistemas legados.