Lambda com .NET: quando serverless realmente faz sentido

Serverless vendeu, em algum momento, a promessa de nunca mais pensar em servidor. Na prática, o que ele faz é trocar um tipo de preocupação por outro: em vez de gerenciar máquina, passa a importar muito o formato da carga de trabalho, porque nem toda carga se encaixa bem no modelo de execução do AWS Lambda.

Onde Lambda com .NET brilha

Lambda cobra por tempo de execução, não por servidor ligado. Isso é uma vantagem clara quando o trabalho é orientado a evento e não acontece o tempo todo: processar uma mensagem que chegou numa fila SQS, reagir a um arquivo que acabou de subir para um bucket S3, responder a uma chamada de API Gateway que só recebe tráfego em picos previsíveis, executar uma rotina agendada uma vez por hora. Manter uma instância ligada 24 horas por dia só para atender esses eventos esporádicos seria pagar por tempo ocioso que ninguém está usando.

O .NET tem suporte nativo como runtime gerenciado para Lambda, e uma função pode ser escrita como um handler relativamente simples:

public class Function
{
    public async Task<string> HandlerAsync(SQSEvent evento, ILambdaContext contexto)
    {
        foreach (var mensagem in evento.Records)
        {
            await ProcessarAsync(mensagem.Body);
        }

        return "ok";
    }
}

Cada invocação recebe o evento (nesse caso, um lote de mensagens do SQS), processa e termina. Não existe um servidor esperando ali entre uma execução e outra.

O cold start ainda é o ponto que mais pesa

O problema que historicamente mais incomodava quem vinha do mundo de aplicação web tradicional é o cold start: quando a função não é chamada há algum tempo, a próxima invocação precisa inicializar o ambiente de execução antes de processar qualquer coisa, o que adiciona latência perceptível logo na primeira resposta.

Em .NET, esse custo de inicialização historicamente pesava mais do que em runtimes mais leves, porque o JIT precisa compilar o código na primeira execução. A compilação Native AOT (ahead-of-time) muda esse cenário: o código já sai compilado para código de máquina nativo no momento do build, então a inicialização da função fica bem mais rápida, o que reduz o cold start de forma bastante significativa em cenários sensíveis a latência.

Vale registrar que Native AOT tem restrições próprias: reflection dinâmica é limitada, algumas bibliotecas que dependem fortemente dela podem precisar de ajuste, e o processo de build fica um pouco mais complexo do que uma publicação tradicional. Não é uma troca sem custo nenhum, é uma troca que costuma valer a pena quando cold start realmente importa para o caso de uso.

Quando Lambda deixa de ser a resposta óbvia

Para uma API síncrona, tradicional, com tráfego relativamente constante ao longo do dia, o modelo de cobrança por execução perde a vantagem: se o volume de chamadas já justificaria manter um servidor ligado o tempo inteiro de qualquer forma, pagar por invocação deixa de ser mais barato e passa a só adicionar a complexidade do cold start sem necessidade.

Também vale desconfiar de Lambda para processamento que precisa rodar por muito tempo em uma única execução. Existe um limite de tempo máximo de execução por invocação, e workloads longos e contínuos tendem a se encaixar melhor num modelo de container ou instância dedicada, sem essa restrição.

A pergunta que decide

Mais do que “Lambda é bom ou ruim”, a pergunta que realmente importa é: esse processamento é acionado por evento, de forma esporádica ou em picos, ou ele precisa ficar ativo constantemente esperando trabalho? A primeira resposta aponta para Lambda. A segunda aponta para container ou instância dedicada. Serverless não é serverless para todo tipo de carga, é serverless para o tipo de carga que ele foi desenhado para atender.