Ao final deste artigo você vai conseguir decidir se uma carga de IA deve rodar em serverless, em containers ou nos dois, e vai ter um esqueleto de arquitetura híbrida com API leve, fila e workers. A primeira pergunta, porém, vem antes de qualquer tecnologia: o modelo roda na sua infraestrutura ou você chama o modelo de um provedor por API?
Primeiro: hospedar o modelo ou chamar uma API
Se a sua aplicação só chama a API de um modelo hospedado por terceiros, o grosso do trabalho (inferência, GPU, carga de pesos) não está com você. Seu código só monta o prompt, espera a resposta e trata o resultado. Nesse caso serverless costuma ser excelente: a função passa a maior parte do tempo esperando a rede, o tráfego irregular não custa nada parado e não há modelo para carregar.
A discussão interessante começa quando o modelo roda por sua conta, seja um modelo aberto, um modelo ajustado por você ou um modelo pequeno de classificação. Aí entram o peso dos arquivos, a memória, a GPU e o tempo de carga. É esse caso que o restante do artigo cobre.
Cold start: o problema do serverless com modelos
Em serverless, quando não há uma instância pronta, o provedor precisa criar um ambiente, baixar o código ou a imagem e inicializar o runtime. Isso é o cold start. Para uma função pequena, são tipicamente frações de segundo a poucos segundos. Para um modelo, o tempo se soma a outras etapas:
- Criar o ambiente de execução.
- Trazer os pesos do modelo de um armazenamento de objetos (S3, por exemplo).
- Carregar os pesos na memória (ou na GPU).
- Inicializar o framework.
Os pesos de um modelo de poucos bilhões de parâmetros ocupam alguns gigabytes mesmo quantizados, e a etapa 2 e a 3 dominam a conta. O tempo total depende do tamanho do modelo, da rede e do disco, e só medindo no seu ambiente você sabe. O padrão do problema é o mesmo em qualquer lugar: a primeira requisição depois de um período ocioso fica muito mais lenta que as seguintes, e o cliente sente.
Há mitigações, como manter instâncias aquecidas (provisioned concurrency no Lambda, instâncias mínimas no Cloud Run), mas elas trazem de volta o custo de ter algo ligado o tempo todo, que era justamente o que o serverless prometia evitar.
Limites técnicos que pesam em IA
No AWS Lambda, os limites documentados que mais afetam IA são:
- Timeout máximo de 15 minutos por invocação.
- Memória de até 10 GB por função, com CPU proporcional.
- Imagens de container de até 10 GB, o que permite empacotar modelos pequenos.
- Nenhuma GPU. Para inferência que precisa de GPU, Lambda não é opção.
- Disco efêmero em
/tmp(configurável até 10 GB), que não se mantém garantidamente entre invocações.
Confira a página de quotas do Lambda antes de projetar, porque esses limites mudam com o tempo. Outro ponto de atenção é o timeout do que fica na frente da função: o API Gateway tem um tempo máximo de integração, e uma resposta lenta de modelo pode estourar com erro 504 antes de a função terminar. Em cargas de IA, o padrão assíncrono (devolver um identificador e entregar o resultado depois) evita esse problema.
Existem opções intermediárias que misturam os dois mundos. Alguns serviços de containers serverless oferecem GPU com escala a zero, como o Cloud Run, e há plataformas especializadas em inferência sob demanda. Elas reduzem o custo de ocioso, mas continuam sujeitas a cold start de carga de modelo, então valem as mesmas perguntas.
Quando serverless ganha
O serverless funciona bem quando a carga é irregular e o trabalho por requisição é leve:
- Orquestração de chamadas a modelos hospedados por API.
- Webhooks de terceiros, com picos imprevisíveis.
- Validação, enfileiramento e pré-processamento.
- Cargas muito esporádicas, com poucas execuções por dia, onde pagar por invocação sai mais barato que manter uma máquina ligada.
Aqui o argumento é qualitativo: sem tráfego, o custo tende a zero e a escala é automática. Para o número exato, use a calculadora do seu provedor com o seu volume. Preços de nuvem mudam, e qualquer valor fixo neste texto envelheceria rápido.
Quando containers ganham
Containers compensam quando há tráfego contínuo ou previsível, necessidade de GPU, modelo grande ou latência baixa e estável:
- Chatbot interno ou de produto com uso o dia todo.
- Processamento em lote em horário fixo.
- Inferência que se beneficia de batching e de manter o modelo carregado na GPU.
Com o modelo já na memória, a latência de cada requisição deixa de incluir a carga dos pesos. Servidores de inferência como vLLM ou Ollama, rodando em container, fazem batching contínuo, o que aumenta bastante a vazão por GPU em comparação com atender uma requisição por vez.
Para decidir entre os dois com números, faça a conta com o seu cenário: requisições por dia, duração média de cada uma, necessidade de GPU, proporção de tempo ocioso e o preço por hora da máquina que você usaria. Se a máquina ficaria ociosa na maior parte do tempo, serverless ou escala a zero tende a vencer. Se ficaria ocupada na maior parte do tempo, container fixo tende a vencer.
Arquitetura híbrida: API leve, fila e workers
Uma forma comum de combinar os dois é deixar a borda em serverless e o trabalho pesado em containers, ligados por uma fila:
Cliente
|
API Gateway + Lambda (valida e enfileira, responde 202)
|
Fila (SQS) (desacopla e absorve picos)
|
Workers em container (GPU, modelo carregado, escala pelo tamanho da fila)
|
Banco / S3 (resultado persistido)
O Lambda que recebe o pedido:
import json
import os
import uuid
import boto3
sqs = boto3.client("sqs")
QUEUE_URL = os.environ["QUEUE_URL"]
def lambda_handler(event, context):
body = json.loads(event["body"])
prompt = body.get("prompt", "")
if not prompt or len(prompt) > 10_000:
return {"statusCode": 400, "body": json.dumps({"erro": "prompt inválido"})}
job_id = str(uuid.uuid4())
sqs.send_message(
QueueUrl=QUEUE_URL,
MessageBody=json.dumps({"job_id": job_id, "prompt": prompt}),
)
return {"statusCode": 202, "body": json.dumps({"job_id": job_id})}
O worker, em container, carrega o modelo uma vez e consome a fila:
import json
import os
import boto3
from transformers import pipeline
sqs = boto3.client("sqs")
table = boto3.resource("dynamodb").Table(os.environ["RESULTS_TABLE"])
QUEUE_URL = os.environ["QUEUE_URL"]
# Carrega o modelo uma vez, na subida do processo
generator = pipeline(
"text-generation",
model=os.environ["MODEL_ID"],
device_map="auto",
)
def process(job):
try:
out = generator(job["prompt"], max_new_tokens=200, return_full_text=False)
table.put_item(Item={
"job_id": job["job_id"],
"status": "done",
"result": out[0]["generated_text"],
})
except Exception as exc:
table.put_item(Item={
"job_id": job["job_id"],
"status": "failed",
"error": str(exc),
})
while True:
resp = sqs.receive_message(
QueueUrl=QUEUE_URL,
MaxNumberOfMessages=5,
WaitTimeSeconds=20,
VisibilityTimeout=300,
)
for msg in resp.get("Messages", []):
process(json.loads(msg["Body"]))
sqs.delete_message(QueueUrl=QUEUE_URL, ReceiptHandle=msg["ReceiptHandle"])
O MODEL_ID vem de variável de ambiente, então você troca o modelo sem mudar o código. O device_map="auto" exige a biblioteca accelerate instalada. Este worker processa um job por vez dentro de cada lote, o que é simples e suficiente para começar. Para vazão maior, o caminho é trocar o laço por um servidor de inferência com batching.
O cliente consulta o resultado por job_id (ou recebe um webhook) quando o status chega a done. Escalar os workers pelo tamanho da fila é uma boa regra: o Kubernetes tem o KEDA para isso, e o ECS aceita escala por métrica da fila.
Armadilhas comuns
Fila sem DLQ. Se um job falha várias vezes e simplesmente some, ninguém descobre. Configure uma dead-letter queue (política de redrive no SQS) e alerta quando ela receber mensagens.
Visibility timeout curto demais. Se o processamento demora mais que o tempo de visibilidade, outra instância pega a mesma mensagem e o trabalho é duplicado. Ajuste o valor ao pior caso de duração e faça o worker ser idempotente por job_id.
Worker com GPU ligada o dia todo sem carga. Escala mínima de zero ou de um, conforme a sensibilidade a latência da primeira requisição, e um desligamento por ociosidade.
Serverless "puro" com modelo grande. Se o modelo não cabe nos limites de memória, de imagem ou de GPU, a conclusão é de arquitetura, não de ajuste fino de timeout.
Resumo da decisão
Use serverless para a borda (validar, orquestrar, chamar APIs) e para cargas esporádicas e leves. Use containers quando há GPU, modelo grande, tráfego contínuo ou latência estável como requisito. Use a combinação com fila quando a carga tem picos e o trabalho pesado não precisa responder na hora.
Próximos passos
Para provisionar fila, workers e escala como código, veja Terraform: primeiros passos. Para ver latência fim a fim e custo por requisição em produção, leia Observabilidade de LLM.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria