Filas e processamento assíncrono para aplicações com IA
Uma chamada a um LLM pode levar alguns segundos, às vezes mais de dez, dependendo do modelo e do tamanho da resposta esperada. Fazer isso dentro do ciclo normal de requisição e resposta de uma API web significa deixar o cliente esperando esse tempo inteiro, com a conexão aberta, torcendo para não bater timeout antes da resposta chegar.
O problema não é a IA ser lenta, é o modelo de requisição síncrona não tolerar espera
Uma API tradicional assume que uma requisição recebe resposta rápida, geralmente em milissegundos. Chamadas de IA quebram essa suposição por padrão, não como exceção. Manter esse padrão síncrono para uma chamada que demora segundos significa: conexão HTTP aberta por mais tempo que o normal, risco real de timeout (do lado do cliente, do load balancer, ou de qualquer proxy no meio do caminho) e uma interface travada esperando, sem forma de mostrar progresso.
Desacoplar com uma fila resolve o lado do produtor
A solução estrutural é não processar a chamada de IA dentro do próprio ciclo da requisição web. Em vez disso, a requisição recebida publica uma mensagem numa fila (SQS, RabbitMQ, ou qualquer fila equivalente) e devolve resposta imediata, confirmando que o pedido foi aceito, sem esperar o processamento terminar. Um worker em segundo plano, separado do processo que atende requisições web, consome a fila e executa a chamada de IA no próprio ritmo, sem pressão de timeout de requisição HTTP.
app.MapPost("/resumos", async (ResumoRequest request, IAmazonSQS sqs) =>
{
var jobId = Guid.NewGuid();
await sqs.SendMessageAsync(new SendMessageRequest
{
QueueUrl = filaUrl,
MessageBody = JsonSerializer.Serialize(new { jobId, request.Texto })
});
return Results.Accepted($"/resumos/{jobId}", new { jobId });
});
O 202 Accepted, com uma URL para consultar o status depois, é o sinal explícito de que a requisição foi aceita mas ainda não foi processada, diferente de um 200 OK que sugeriria trabalho já concluído.
Asynchronous Request-Reply: como o cliente descobre que terminou
Devolver 202 Accepted resolve o lado do produtor, mas deixa uma pergunta aberta: como o cliente sabe quando o resultado está pronto? O padrão Asynchronous Request-Reply, comum em arquiteturas de API assíncronas, resolve isso de duas formas típicas. A primeira é polling: o cliente consulta periodicamente um endpoint de status (GET /resumos/{jobId}) até o processamento aparecer como concluído. A segunda é notificação ativa: o servidor avisa o cliente assim que o resultado fica pronto, em vez do cliente ficar perguntando em intervalos.
SignalR para o caso de notificação ativa
Para aplicações web com conexão já estabelecida com o cliente, SignalR permite notificar o front-end assim que um job termina, sem exigir que o cliente fique reconsultando um endpoint em loop. O worker, ao terminar o processamento, publica o resultado num hub, que empurra a atualização para o cliente conectado.
public class ResumoWorker(IHubContext<ResumoHub> hubContext)
{
public async Task ProcessarAsync(Guid jobId, string texto, CancellationToken cancellationToken)
{
var resumo = await GerarResumoAsync(texto, cancellationToken);
await hubContext.Clients.Group(jobId.ToString())
.SendAsync("ResumoConcluido", resumo, cancellationToken);
}
}
Isso elimina o polling do lado do cliente, ao custo de manter uma conexão persistente (WebSocket, ou fallback do SignalR) enquanto o cliente espera pela resposta.
Isso não é otimização prematura, é evitar travar a interface
Desacoplar chamada de IA do ciclo síncrono de requisição não é um ajuste de performance opcional para quando o sistema crescer. É o que evita uma interface travada, um timeout constante, e um usuário sem forma de saber se o pedido dele ainda está sendo processado ou já falhou silenciosamente em algum lugar. Para qualquer chamada de IA que não seja trivialmente rápida, essa separação entre aceitar o pedido e processar o pedido deveria ser o padrão, não a exceção tratada só quando o problema já apareceu em produção.