Quem trabalha com Lambda conhece a história: você tem um processo que precisa esperar algo. Uma aprovação humana, uma resposta de uma API lenta, uma operação que vai demorar mais do que os famosos 15 minutos de timeout. A solução tradicional? Step Functions. Funciona bem, mas é mais infraestrutura pra gerenciar, mais estado pra coordenar, mais dinheiro saindo do bolso.
O Durable Functions chegou pra mudar isso. Não é uma feature nova se você vem do mundo Azure, onde o conceito já existe há anos. Mas agora está disponível no AWS Lambda, e a proposta é simples: escrever código sequencial que parece síncrono, mas que consegue pausar, dormir, e retomar depois de dias se necessário. Tudo isso sem você pagar pelo tempo de espera.
O cenário mais interessante que vejo? Integrações com LLMs. Pense em um agente que precisa chamar várias ferramentas, esperar respostas humanas no meio do caminho, e manter contexto por horas. Com Durable Functions, você não precisa de uma arquitetura de filas complexa. Simplesmente funciona.
Como a Mágica Acontece: Checkpoint e Replay
A coisa é: o Durable Functions não mantém sua Lambda rodando por horas. Seria caro demais. O que ele faz é um mecanismo de checkpoint e replay.
Quando você chama um context.step(), o Lambda registra o resultado em um log. Se a função precisa pausar, ela salva esse checkpoint e simplesmente para. Quando retoma, a função é invocada de novo do zero. Isso mesmo, do início. Mas aqui está o truque: os steps que já executaram não rodam de novo. O sistema recupera os valores do log de checkpoint e segue em frente.
É elegante, mas tem uma implicação crucial: seu código precisa ser determinístico. Se você usar datetime.now() fora de um step pra tomar uma decisão, vai ter problemas. No replay, o valor vai ser diferente, e a execução pode divergir do caminho original. O mesmo vale pra números aleatórios, UUIDs, qualquer coisa que muda entre execuções.
Na Prática: Um Workflow de Aprovação
Montei uma demo que ilustra bem o conceito. É um sistema de aprovação de pedidos onde a função literalmente para e espera um humano clicar num botão.
O workflow tem cinco etapas: criar o pedido no DynamoDB, validar os dados, esperar aprovação, processar ou cancelar conforme a decisão, e notificar. A parte interessante é a terceira etapa.
callback = context.create_callback(
name="approval_callback",
config=CallbackConfig(timeout=Duration.from_minutes(5)),
)
# Salva o callback_id no banco pra o handler de aprovação encontrar
table.update_item(
Key={"orderId": order["orderId"]},
UpdateExpression="SET callbackId = :callbackId",
ExpressionAttributeValues={":callbackId": callback.callback_id},
)
# Aqui a execução suspende. Você não paga nada enquanto espera.
approval_result = callback.result()
Quando esse callback.result() é chamado, a Lambda simplesmente para de executar. Não tem instância rodando, não tem cobrança. Pode ficar assim por minutos ou horas. Quando alguém aprova o pedido via API, outra função chama send_durable_execution_callback_success() com o resultado, e a função original retoma exatamente de onde parou.
Cada operação que tem efeito colateral está dentro de um @durable_step. Criar pedido, validar, processar, cancelar, notificar. Isso garante que se der um problema no meio e a função precisar reiniciar, os passos já completados não vão executar de novo.
Dissecando o Código
Vamos passar pelo workflow completo. Começa pelos imports do SDK:
from aws_durable_execution_sdk_python import (
DurableContext,
durable_execution,
durable_step,
StepContext,
)
from aws_durable_execution_sdk_python.config import CallbackConfig, Duration
O DurableContext é o objeto que você recebe no handler e usa pra todas as operações duráveis. O durable_execution é o decorator que transforma sua função num workflow durável. O durable_step marca funções que devem ser checkpointed. O StepContext é injetado automaticamente nos steps.
Cada step é uma função decorada:
@durable_step
def create_order(step_context: StepContext, event: dict) -> dict:
order_id = str(uuid.uuid4())
order = {
"orderId": order_id,
"customerName": body.get("customerName", "Unknown"),
"status": "pending_approval",
"createdAt": datetime.utcnow().isoformat(),
}
table.put_item(Item=order)
return order
Repara que o uuid.uuid4() e o datetime.utcnow() estão dentro do step. Isso é proposital. Se estivessem fora, no replay teriam valores diferentes. Dentro do step, o resultado inteiro é persistido no checkpoint, então no replay o SDK simplesmente retorna o order que foi salvo da primeira vez.
O handler principal orquestra tudo:
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
# Step 1: Criar pedido
order = context.step(create_order(event))
# Step 2: Validar
context.step(validate_order(order))
# Step 3: Criar callback e esperar
callback = context.create_callback(
name="approval_callback",
config=CallbackConfig(timeout=Duration.from_minutes(5)),
)
# Armazena o callback_id pra outra função poder responder
table.update_item(
Key={"orderId": order["orderId"]},
UpdateExpression="SET callbackId = :callbackId",
ExpressionAttributeValues={":callbackId": callback.callback_id},
)
# Suspende aqui
approval_result = callback.result()
# Step 4: Processa ou cancela
if approval_result.get("approved"):
context.step(process_order(order))
else:
context.step(cancel_order(order))
# Step 5: Notifica
context.step(send_notification(order, approved))
return {"statusCode": 200, "body": json.dumps({...})}
O context.step() é como você executa um durable step. Ele chama a função, persiste o resultado, e no replay recupera direto do checkpoint sem re-executar.
O context.create_callback() cria um ponto de espera. O callback.result() é onde a função suspende. O SDK gera um callback_id único que você precisa armazenar em algum lugar, pra que outra função consiga enviar a resposta depois.
A parte do branching após o callback é interessante: o if approval_result.get("approved") vai rodar no replay também, mas como o approval_result veio do checkpoint, a decisão vai ser a mesma. Determinismo preservado.
Os Gotchas Que Você Vai Encontrar
O Yan Cui escreveu sobre cinco armadilhas do Durable Functions, e vale conhecer antes de sair usando em produção.
A primeira é o código não-determinístico que mencionei. Se você usa timestamps ou random pra tomar decisões de branching fora de steps, prepare-se pra comportamento estranho. A solução é capturar essas decisões dentro de steps, onde ficam registradas no checkpoint.
A segunda é efeitos colaterais fora de steps. Atualizar banco, chamar API externa, enviar email, tudo isso precisa estar num step. Senão, no replay, vai executar de novo. Imagine mandar o mesmo email três vezes porque a função sofreu replay duas vezes.
A terceira é mais sutil: mutar variáveis de closure dentro de um step. O SDK não persiste essas mutações. No replay, a variável vai ter o valor original, não o modificado. O código parece funcionar nos testes, mas em produção pode dar comportamento bizarro.
A quarta é usar nomes dinâmicos pra steps. Se o nome do step muda entre execuções, o sistema não consegue encontrar o resultado no checkpoint. Sempre use nomes estáticos e previsíveis.
A quinta é mais técnica: resultados maiores que 256kb em child contexts não são armazenados. Se você usa parallel() ou map() e o resultado passa desse limite, no replay o contexto vai re-executar. Se tinha código não-durável ali dentro, vai rodar de novo.
Sobre Custo e Quando Usar
O ponto forte aqui é custo. Você não paga pelos 15 minutos, 1 hora, ou 24 horas que a função fica esperando. O ExecutionTimeout pode ser configurado até um ano. Um ano. Pense nas possibilidades.
Mas não é bala de prata. Se você tem um workflow complexo com muitos branches paralelos e lógica condicional elaborada, Step Functions ainda pode fazer mais sentido pela visualização e debugging. O Durable Functions brilha quando você quer simplicidade: código linear que precisa de pausas longas.
O cenário de LLMs que mencionei no início é perfeito. Um agente conversacional que precisa de human-in-the-loop, processos que misturam automação com aprovações manuais, integrações com sistemas lentos. Tudo isso fica mais simples de escrever e mais barato de rodar.
Conclusão
O AWS Lambda Durable Functions preenche uma lacuna que existia há anos. Não é revolucionário pra quem conhece o equivalente do Azure, mas ter isso disponível nativamente no ecossistema AWS, com SAM e CloudFormation, facilita muito a adoção.
A demo que montei mostra o padrão mais comum: workflow que precisa de aprovação humana. Três funções, uma tabela DynamoDB, e um frontend simples. O pedido é criado, a função pausa esperando callback, e retoma quando alguém decide. O código é linear, fácil de entender, e você não paga pelos minutos de espera.
Se você está batendo nos limites do Lambda tradicional ou gastando demais com Step Functions pra casos simples, vale muito a pena experimentar.
Insights & Takeaways
Checkpoint e replay é o mecanismo central: a função não fica rodando, ela para e reinicia do zero quando retoma, recuperando resultados já computados do log de checkpoint.
Determinismo é obrigatório: timestamps, random, e qualquer valor que muda entre execuções precisa ser capturado dentro de steps, ou você terá comportamento inconsistente no replay.
Efeitos colaterais só dentro de steps: chamadas de API, atualizações de banco, envio de notificações - tudo que não deve repetir precisa estar encapsulado em um
@durable_step.Você não paga pelo tempo de espera: quando a função suspende aguardando callback ou wait, não tem cobrança. Isso muda completamente a economia de processos longos.
LLMs e human-in-the-loop são casos de uso ideais: a combinação de execuções longas, pausas para intervenção humana, e código linear simples é exatamente onde Durable Functions brilha.
