Cache e Limitação de Taxa (Rate Limiting)
Por que cache e limitação de taxa importam em sistemas de IA
Sistemas modernos de IA — especialmente aplicações apoiadas por LLMs (large language models) — frequentemente têm curvas não lineares de custo e latência:
- O custo é frequentemente proporcional ao uso (por exemplo, tokens processados, tempo de GPU, chamadas de ferramentas).
- A latência é impulsionada por gargalos compartilhados (vazão de GPU, recuperação de banco de dados/RAG, APIs externas).
- O tráfego é intermitente (lançamentos de produto, jobs em lote, tentativas de novo, “refresh spam”), produzindo latências de cauda e indisponibilidades.
Cache e limitação de taxa (rate limiting) são ferramentas complementares para manter sistemas estáveis e previsíveis:
- Cache reduz trabalho repetido (computação, recuperação, chamadas de ferramentas) e melhora a latência mediana.
- Limitação de taxa e cotas limitam a demanda no pior caso, protegem recursos compartilhados e impõem equidade e orçamentos.
- Modelagem de requisições (request shaping) (enfileiramento, processamento em lote, priorização) suaviza picos e melhora a vazão.
Em Engenharia de IA/MLOps (Machine Learning Operations), essas técnicas são alavancas centrais junto de escalonamento e otimização (veja Custo/Desempenho, Otimização de Inferência e Servir Modelos).
A troca central: recalcular vs reutilizar vs rejeitar
Você pode enxergar a inferência em produção como um problema de controle:
- Reutilizar (cache) quando os resultados provavelmente serão idênticos ou “próximos o suficiente”.
- Recalcular quando frescor ou correção importam.
- Rejeitar / atrasar (limitar taxa, enfileirar, descartar carga) quando a demanda ameaça a confiabilidade ou os orçamentos.
Um sistema robusto normalmente usa os três.
Cache: fundamentos e padrões
O que “cache” significa em aplicações de IA
Um cache armazena resultados de operações caras para que requisições futuras possam ser atendidas mais rápido e com menor custo. Em sistemas de IA, essas “operações” não são apenas chamadas ao modelo:
- Conclusões de LLM (LLM completions) (resposta completa ou parcial)
- Embeddings (para documentos/consultas repetidos)
- Resultados de recuperação em RAG (RAG retrieval results) (IDs dos documentos top-k e trechos)
- Resultados de reranqueamento (reranker results)
- Resultados de chamadas de ferramenta/API (tool/API call results) (busca, consultas ao banco de dados)
- Cálculo de atributos (feature computation) (para modelos clássicos de ML)
- Compilação de prompt (prompt compilation) (template + dados do usuário + política do sistema)
Em pilhas de LLM, cache frequentemente gera retornos desproporcionais porque os mesmos passos caros se repetem entre usuários (por exemplo, o mesmo prefixo de prompt do sistema, a mesma consulta na base de conhecimento, a mesma pergunta de “ajuda”).
Hierarquia de cache: onde o cache pode existir
Sistemas de IA geralmente se beneficiam de múltiplas camadas de cache:
- Cache no lado do cliente (client-side cache) (browser/app): ótimo para endpoints idempotentes (por exemplo, “listar modelos”, “chats recentes”).
- Cache de CDN / borda (edge cache): útil para ativos estáticos e algumas APIs públicas com muita leitura.
- Cache no gateway de API (API gateway cache): cache granular grosso para certas rotas.
- Cache no nível do serviço (service-level cache) (Redis/Memcached): compartilhado entre réplicas; comum para respostas de LLM e recuperação em RAG.
- Cache local em memória do processo (process-local in-memory cache) (LRU, least recently used): ultra-rápido, mas não compartilhado; melhor para chaves quentes, objetos pequenos.
- Caches do mecanismo de inferência (inference-engine caches): por exemplo, cache de prefixo/KV (key-value) dentro do serving de LLM (veja Serving de LLM).
Uma abordagem prática é: L1 em memória (rápido, pequeno) + L2 cache compartilhado (maior, consistente).
O que torna o cache de LLM complicado: não determinismo e contexto
Ao contrário de muitos endpoints tradicionais, as saídas de LLM dependem de:
- Modelo e versão
- Conteúdo completo do prompt (mensagens do sistema + desenvolvedor + usuário)
- Parâmetros de geração (temperature, top_p, max_tokens, seed se suportado)
- Definições de ferramentas / esquemas de função
- Documentos recuperados (RAG) e suas versões
- Filtros/políticas de segurança e suas versões
Se qualquer um desses mudar, resultados em cache podem ser inválidos ou inseguros.
Regra geral: faça cache apenas quando você consegue definir uma relação de equivalência clara para “mesma requisição”.
Chaves de cache: o “contrato” de correção
Uma chave de cache deve identificar unicamente a computação. Para conclusões de LLM, o material da chave normalmente inclui:
model_id(e possivelmente deployment/região)- Prompt/mensagens (canonizados)
- Parâmetros de geração (
temperature,top_p,max_tokens, etc.) - Versões do esquema de ferramentas
- Identificadores de snapshot de RAG (versão do índice, IDs de docs + versões, parâmetros de recuperação)
- Versão da política de segurança
- Versão do seu aplicativo (ou versão do template de prompt)
Considere também:
- Canonização (canonicalization): normalize espaços em branco, formatação JSON e ordenação (por exemplo, esquemas de ferramentas) para que requisições logicamente idênticas mapeiem para a mesma chave.
- Hashing: armazene um hash em vez do prompt bruto para evitar chaves grandes e reduzir exposição de dados sensíveis.
Exemplo: construindo uma chave robusta de cache de LLM (Python)
import hashlib
import json
def canonical_json(obj) -> str:
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def llm_cache_key(payload: dict) -> str:
# payload should already include all relevant fields:
# model, messages, params, tool_schema_version, rag_snapshot_id, etc.
s = canonical_json(payload)
h = hashlib.sha256(s.encode("utf-8")).hexdigest()
return f"llm:v1:{h}"
TTLs, invalidação e obsolescência
Fazer cache é fácil até você precisar de frescor. Estratégias comuns:
- Expiração baseada em TTL (time-to-live): simples e eficaz para respostas “majoritariamente estáveis” (por exemplo, FAQ, ajuda de documentação).
- Chaves versionadas: inclua
prompt_version,policy_version,index_versionoumodel_versionpara forçar misses quando houver mudança. - Invalidação explícita: delete chaves quando o conteúdo subjacente muda (mais difícil de fazer corretamente em escala).
Para sistemas RAG, um bom padrão é recuperação versionada (versioned retrieval):
- inclua
kb_index_versione parâmetros de recuperação na chave - quando você reconstrói o índice, a versão muda e automaticamente estoura o cache
Stampede de cache: quando o cache aumenta a carga
Se uma chave popular expira e muitas requisições dão miss simultaneamente, você pode sobrecarregar o backend (“thundering herd”). Mitigações:
- Coalescência de requisições / single-flight: apenas uma requisição computa; as outras aguardam.
- Stale-while-revalidate: sirva dados levemente obsoletos enquanto uma atualização em segundo plano roda.
- Atualização antecipada probabilística (probabilistic early refresh): atualize antes da fronteira do TTL para chaves quentes.
O que colocar em cache em aplicações de LLM (menu prático)
1) Cache de conclusões por correspondência exata
Melhor quando:
- prompts se repetem exatamente (ou podem ser canonizados)
- a geração é determinística (por exemplo,
temperature=0) - a tolerância a correção é rigorosa
Casos de uso:
- templates de prompt para tarefas estruturadas (classificação, extração)
- checagens de política ou geração de rubricas
- passos internos repetidos de agentes com entradas idênticas
2) Cache semântico (semantic caching) (correspondência aproximada)
Em vez de igualdade exata do prompt, você faz cache por significado:
- gere o embedding do prompt/consulta de entrada
- pesquise em um armazenamento vetorial (vector store) por prompts anteriores similares
- se a similaridade exceder um limiar, reutilize a resposta em cache
Isso pode reduzir drasticamente o custo em cargas “tipo FAQ”, mas introduz risco de aproximação.
Mitigações:
- limiares altos de similaridade
- habilitar apenas para endpoints de baixo risco
- fazer cache apenas de resultados intermediários (por exemplo, resultados de recuperação), não de respostas finais
- anexar proveniência (o que foi correspondido) para depuração
3) Cache do pipeline de RAG
Sistemas RAG têm múltiplos pontos de cache:
- Cache de embedding de consulta: mesmo texto de consulta → mesmo embedding.
- Cache de recuperação:
(query_embedding, filters, top_k, index_version)→ IDs de documentos/trechos. - Cache de reranqueamento:
(query, candidate_doc_ids, reranker_version)→ ordenação pontuada. - Cache de resposta final: inclui IDs de docs + versões na chave para evitar servir respostas fundamentadas em documentos desatualizados.
Fazer cache da recuperação frequentemente traz ganhos fortes porque a latência de recuperação pode ser comparável à latência de geração em produção.
4) Cache de ferramentas/resultados
Se seu agente chama ferramentas (busca na web, consultas a DB, APIs internas), muitos resultados são cacheáveis com TTL:
- resultados de busca: TTL curto (minutos)
- dados de referência internos: TTL maior (horas)
- dados específicos do usuário: chaves de cache por usuário + controles de privacidade mais rígidos
Isso é uma alavanca importante em Padrões de Design de Sistemas com LLM porque chamadas de ferramentas podem dominar tanto latência quanto custo.
Exemplo: cache exato de conclusões com Redis (pseudo-implementação)
import redis
import json
import time
r = redis.Redis(host="redis", port=6379, decode_responses=True)
def get_or_compute_llm(payload, compute_fn, ttl_seconds=3600):
key = llm_cache_key(payload)
cached = r.get(key)
if cached:
obj = json.loads(cached)
return obj["response"], True
# Optional: single-flight lock to avoid stampede
lock_key = key + ":lock"
got_lock = r.set(lock_key, "1", nx=True, ex=30)
if not got_lock:
# Someone else is computing: wait briefly and retry
time.sleep(0.2)
cached = r.get(key)
if cached:
obj = json.loads(cached)
return obj["response"], True
response = compute_fn(payload)
r.set(key, json.dumps({"response": response}), ex=ttl_seconds)
r.delete(lock_key)
return response, False
Considerações de segurança, privacidade e multi-inquilino
Cache pode vazar dados se você errar as fronteiras de tenancy.
- Sempre inclua o escopo de inquilino/usuário (tenant/user) na chave de cache para qualquer requisição personalizada.
- Evite armazenar prompts brutos contendo PII; armazene hashes e/ou criptografe valores.
- Considere caches separados por ambiente (prod/staging) e por região.
- Tenha cuidado ao fazer cache de saídas do modelo que incluem dados sensíveis do usuário; aplique sua política de logging/privacidade de forma consistente (veja Privacidade em Logs).
Limitação de taxa, cotas e modelagem de requisições
Cache reduz a carga média. Limitação de taxa garante que a carga não pode exceder limites seguros.
Objetivos da limitação de taxa em sistemas de IA
- Confiabilidade: prevenir sobrecarga e falhas em cascata.
- Equidade: evitar que um usuário/inquilino consuma toda a capacidade.
- Controle de custo: impor orçamentos de tokens e prevenir contas surpresa.
- Conformidade com SLO: manter latência de cauda sob controle (veja SLOs para Funcionalidades de IA).
- Prevenção de abuso: mitigar scraping, força bruta, inundação de prompts.
Primitivas comuns de limitação de taxa
A limitação de taxa geralmente é aplicada em uma ou mais dimensões:
- Requisições por segundo/minuto (RPS/RPM)
- Requisições concorrentes (limite in-flight)
- Tokens por minuto (TPM) ou unidades de computação
- Chamadas de ferramenta por minuto (busca, DB)
- Cotas diárias/mensais (orçamentos alinhados a cobrança)
Em cargas de trabalho com LLM, tokens e concorrência costumam ser mais significativos do que contagem bruta de requisições.
Algoritmos (da teoria à prática)
Contador de janela fixa (fixed window counter)
- Conte requisições em uma janela de tempo (por exemplo, por minuto).
- Simples, mas tem artefatos de fronteira (picos nas bordas da janela).
Janela deslizante / log contínuo (sliding window / rolling log)
- Mais preciso, mais caro de implementar em escala.
Balde de tokens (token bucket) (recomendado para muitas APIs)
- O balde enche a uma taxa constante; cada requisição consome tokens.
- Permite picos até o tamanho do balde enquanto impõe a taxa de longo prazo.
Balde com vazamento (leaky bucket)
- Impõe uma taxa de saída suave (útil para modelagem/enfileiramento).
Limitação de concorrência (concurrency limiting)
- Limita trabalho in-flight; geralmente é o mais diretamente ligado à saturação de GPU e à latência de cauda.
Na prática, sistemas frequentemente combinam balde de tokens (taxa) + limite de concorrência (segurança).
Exemplo: limitador por balde de tokens (Python conceitual)
import time
from dataclasses import dataclass
@dataclass
class TokenBucket:
capacity: float
refill_rate: float # tokens per second
tokens: float
last: float
def allow(self, cost: float = 1.0) -> bool:
now = time.time()
elapsed = now - self.last
self.last = now
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
if self.tokens >= cost:
self.tokens -= cost
return True
return False
Para sistemas distribuídos, você normalmente implementaria isso em:
- um gateway de API (Envoy, NGINX, Kong, etc.)
- ou um armazenamento compartilhado (Redis) usando operações atômicas/scripts Lua
Cotas: impondo orçamentos em horizontes mais longos
Cotas são “limitação de taxa ao longo de um período de cobrança”:
- Por usuário: 100k tokens/dia
- Por organização: $500/mês
- Por tier de modelo: modelos menores quase ilimitados, modelos grandes com limite
Em sistemas de LLM, cotas frequentemente acompanham:
- tokens de prompt
- tokens de conclusão
- chamadas de ferramentas
- segundos de imagem/áudio
Uma boa prática é reservar orçamento antes da execução e reconciliar depois:
- Estime o custo a partir do tamanho da entrada e de
max_tokens. - Verifique a cota; se permitido, reserve.
- Após a conclusão, registre o uso real e ajuste.
Isso evita que “uma requisição enorme” estoure seu orçamento mesmo se der timeout no meio da execução.
Modelagem de requisições: enfileiramento, priorização, processamento em lote
Limitação de taxa é um “portão rígido”. Modelagem de requisições é “como lidamos com o que aceitamos”.
Enfileiramento e backpressure
- Coloque requisições em uma fila limitada quando o sistema está ocupado.
- Aplique timeouts; falhe rapidamente quando a fila estiver cheia.
- Retorne
429 Too Many Requestsou503 Service Unavailablecom um headerRetry-After.
Filas prioritárias (priority queues)
Nem todas as requisições são iguais:
- tráfego interativo de usuários > jobs em lote em segundo plano
- tier pago > tier gratuito
- checagens críticas de segurança > melhorias opcionais
Processamento em lote (batching)
Batching melhora a vazão de GPU ao amortizar overhead:
- combine múltiplos prompts em um único lote no servidor do modelo
- ou use recursos de batching dinâmico nas pilhas de serving (veja Serving de LLM)
Batching é uma ferramenta de modelagem: pode reduzir custo por requisição, mas pode aumentar latência se você esperar para formar lotes. Muitos sistemas usam janelas pequenas de batching (por exemplo, 5–20 ms) para tráfego interativo.
Descarte de carga e degradação graciosa
Quando você se aproxima da saturação, muitas vezes é melhor degradar graciosamente do que colapsar:
- rotear para um modelo menor/mais barato
- reduzir max tokens
- desabilitar ferramentas opcionais
- mudar de “best-of-n” para uma única amostra
- servir respostas em cache/obsoletas para consultas de baixo risco
Esses são padrões comuns em Padrões de Design de Sistemas com LLM.
Projetando cache e limitação de taxa em conjunto
Uma arquitetura prática
Uma configuração comum em produção:
Borda / gateway de API
- balde de tokens por IP/por usuário
- limite global de concorrência
- cache grosseiro opcional para endpoints GET públicos
Serviço de aplicação
- cache por correspondência exata para passos determinísticos
- cache semântico para endpoints de baixo risco (opcional)
- cache de recuperação de RAG
- caches de chamadas de ferramentas com TTL
- cotas por inquilino (tokens/dia, $/mês)
Camada de serving do modelo
- batching dinâmico
- cache de prefixo/KV (quando suportado)
- limites de tamanho de fila + timeouts
Observabilidade
- taxa de acerto de cache por camada
- uso de tokens e custo por inquilino/endpoint
- taxas de 429/503 e comportamento de retry
- percentis de latência e métricas de saturação
Isso se conecta naturalmente a Observabilidade para Apps com LLM e Monitoramento.
Métricas que importam
Para cache:
- taxa de acerto (hit rate) (geral e por endpoint/modelo)
- taxa de acerto em bytes (byte hit rate) (especialmente se as respostas variam em tamanho)
- latência p50/p95 com e sem cache
- atendimentos obsoletos (stale serves) (se usar stale-while-revalidate)
- eventos de stampede (contenção de lock, misses concorrentes)
Para limitação de taxa / cotas:
- taxa de 429 e amplificação por retry
- tamanho de fila e tempo na fila
- utilização de concorrência em servidores de modelo
- tokens por minuto e custo por minuto
- principais inquilinos por uso (para equidade)
Modos de falha e como evitá-los
- Cache de resultados incorretos: campos faltando na chave (versão do modelo, snapshot de recuperação, parâmetros).
- Cache de resultados inseguros: política de segurança muda, mas respostas em cache persistem.
- Vazamento de dados entre inquilinos: chave de cache não inclui escopo de inquilino/usuário.
- Tempestades de retry: clientes tentam novamente imediatamente em 429/503; sempre retorne
Retry-Aftere incentive backoff exponencial com jitter. - Limitação excessiva: bloqueio de picos legítimos; considere balde de tokens com capacidade de burst e pistas separadas para interativo vs lote.
- Limitação insuficiente: limitar apenas RPS enquanto uso de tokens explode; adicione controles baseados em tokens.
Checklist prático
Checklist de cache
- Defina o que significa “requisição equivalente” e codifique isso na chave.
- Inclua versões: modelo, template de prompt, política de segurança, índice de RAG.
- Decida TTLs por tipo de cache; evite “cache infinito” a menos que o conteúdo seja imutável.
- Previna stampedes (locks single-flight, stale-while-revalidate).
- Garanta isolamento de inquilinos e armazenamento seguro para privacidade.
- Acompanhe taxa de acerto e economia de custo; remova caches que não se pagam.
Checklist de limitação de taxa e modelagem
- Limite na dimensão correta: tokens/minuto e concorrência frequentemente superam RPS bruto.
- Use um gateway para controle grosseiro e o app para cotas com consciência de inquilino.
- Separe tráfego interativo de jobs em lote (pistas prioritárias).
- Retorne
Retry-Aftere documente o comportamento de backoff para clientes. - Adicione caminhos de degradação graciosa (modelo menor, max tokens reduzido, fallbacks em cache).
- Monitore sinais de saturação e ajuste limites para cumprir seus SLOs.
Quando não fazer cache (ou fazer com cautela)
Cache nem sempre é apropriado:
- Respostas altamente personalizadas que incluem dados do usuário (risco de vazamento, baixo reuso)
- Fatos que mudam rapidamente onde obsolescência é prejudicial (finanças, resposta a incidentes)
- Conteúdo sensível à segurança onde políticas e filtros evoluem rapidamente
- Endpoints de geração criativa onde usuários esperam variação (temperature alta)
Nesses casos, foque mais em cotas, controles de concorrência e modelagem, e considere fazer cache apenas de resultados intermediários (como recuperação ou embeddings).
Resumo
Cache e limitação de taxa são técnicas fundamentais de Engenharia de IA para controle de custo e latência:
- Cache reduz trabalho repetido em chamadas de LLM, recuperação em RAG, embeddings e ferramentas — melhorando latência e reduzindo gastos.
- Limitação de taxa e cotas impõem equidade e orçamentos enquanto previnem sobrecarga.
- Modelagem de requisições (enfileiramento, priorização, batching) melhora a vazão e mantém a latência de cauda administrável.
Quando bem implementados, esses mecanismos transformam uma funcionalidade de IA cara e sujeita a picos em um serviço previsível, com custo controlável, desempenho estável e metas de confiabilidade aplicáveis.