Blog

Aquecimento de índice vetorial: por que a primeira consulta do dia é lenta

Existe um sintoma que quase todo sistema de RAG em produção tem e quase nenhum time mede: a primeira consulta da manhã demora oito segundos e a segunda demora quatrocentos milissegundos. O painel mostra a média do dia e a média está ótima, então ninguém investiga. Só que essa primeira consulta não é um caso raro: ela acontece toda vez que o processo reinicia, toda vez que o autoscaler sobe uma réplica nova, toda vez que a máquina fica ociosa por vinte minutos e o sistema operacional devolve as páginas do índice para o disco. Quem paga é o primeiro cliente depois de cada evento desses, e como esses eventos se concentram nos horários de menor tráfego, o cliente que mais sofre é justamente o que estava sozinho na fila. Este artigo mostra onde exatamente esse tempo é gasto, por que aquecer não é apenas rodar uma consulta boba no boot, como escolher entre pré-carregar o índice na memória e aceitar a latência do primeiro acesso, e como medir isso com uma métrica que não seja apagada pela média.

2026-08-11 / IA Aplicada / 14 min

01

O tempo não está no modelo, está em quatro caches frios

Quando alguém diz que a primeira consulta é lenta, o palpite imediato costuma ser o modelo de embedding. Às vezes é, mas quase nunca é só ele. Uma consulta de RAG atravessa uma cadeia de componentes que têm estados frios independentes, e cada um deles cobra o próprio pedágio na primeira execução. Se você otimizar o componente errado, o gráfico não muda e o time conclui que o problema é inevitável.

A separação útil é por natureza do custo. Há custo de carregar bytes do disco para a memória, há custo de compilar ou alocar estruturas na primeira chamada, há custo de estabelecer conexão e há custo de construir estruturas auxiliares de busca que o índice mantém em memória. Cada um responde a uma técnica diferente de aquecimento, e é por isso que uma única consulta sintética no boot resolve parcialmente e engana bastante.

Componente frioO que custa na primeira vezOrdem de grandeza típicaO que aquece de verdade
Modelo de embedding localCarregar pesos, alocar buffers e compilar kernels do runtime1 a 10 sUma inferência real com o mesmo formato de entrada do tráfego
API de embedding remotaHandshake TLS e resolução de DNS sem pool aberto100 a 500 msAbrir o pool de conexões no boot e mantê-lo vivo com keep-alive
Páginas do índice no discoLer do disco os blocos do grafo ou das listas invertidas0,5 a 30 s conforme o tamanhoTocar as páginas de fato, com consultas que percorrem o índice
Estruturas em memória do motorMontar grafo de navegação, tabelas de quantização e mapas de id0,2 a 5 sChamada explícita de load ou warmup exposta pelo motor
Cache do sistema operacionalReler arquivo mapeado que foi devolvido por falta de usoReaparece após ociosidadeConsulta periódica de manutenção, não só no boot
Camada de rerankingCarregar um segundo modelo, quase sempre esquecido1 a 5 sAquecer com um par consulta-documento representativo

A linha do cache do sistema operacional é a que mais gera discussão, porque contraria a intuição de que aquecer é uma tarefa de inicialização. Um índice grande acessado por arquivo mapeado vive na memória por cortesia do sistema operacional, e essa cortesia é revogada assim que outro processo precisa de memória ou assim que aquelas páginas ficam tempo demais sem uso. Em um serviço que recebe tráfego contínuo isso raramente acontece; em um serviço com tráfego irregular, acontece todo dia de madrugada, e é exatamente esse o caso em que o time percebe o sintoma.

02

Aquecer não é rodar uma consulta qualquer

O aquecimento ingênuo é uma consulta fixa no boot, com um texto qualquer, geralmente a palavra "teste". Ela funciona bem para o modelo de embedding, porque a inferência é a mesma independentemente do texto, e funciona muito mal para o índice, porque uma busca por vizinho aproximado só toca a parte do grafo que fica perto daquele vetor. Se o índice tem oito milhões de vetores e você aquece com um ponto, aqueceu a vizinhança de um ponto. A segunda consulta real, sobre outro assunto, continua fria.

A regra que resolve isso é usar consultas de aquecimento que sejam representativas da distribuição do tráfego real, não sintéticas. Elas existem de graça: pegue as consultas mais frequentes das últimas semanas, agrupe por tema e escolha um punhado que cubra as regiões densas do espaço. O objetivo não é validar a resposta, é forçar o percurso pelo grafo em regiões diferentes para que as páginas correspondentes subam para a memória.

// warmup/index-warmup.js
// Aquecimento com consultas representativas do trafego real, nao sinteticas.
// Roda no boot e depois periodicamente, porque a paginacao do SO esfria
// o indice de novo durante janelas de ociosidade.

const WARMUP_QUERIES = [
  // Uma por regiao densa do espaco. Extraidas do log de consultas reais,
  // agrupadas por tema e revisadas quando a base muda de forma relevante.
  'como faco para cancelar meu pedido',
  'qual o prazo de entrega para o nordeste',
  'quero trocar o produto por defeito',
  'como emitir a segunda via do boleto',
  'a maquininha nao conecta no wifi',
  'quais formas de pagamento voces aceitam',
];

const IDLE_THRESHOLD_MS = 5 * 60 * 1000;

export function createWarmup({ embed, search, rerank, logger, now = () => Date.now() }) {
  let lastRealQueryAt = now();
  let running = false;

  // Chamado pelo caminho de consulta real: se ha trafego, nao ha o que aquecer.
  function markRealQuery() {
    lastRealQueryAt = now();
  }

  async function warmOnce({ reason }) {
    if (running) return { skipped: 'ja em execucao' };
    running = true;
    const startedAt = now();
    let touched = 0;

    try {
      for (const text of WARMUP_QUERIES) {
        // A ordem importa: embedding aquece o modelo, search aquece o grafo
        // e o rerank aquece o segundo modelo, que quase sempre e esquecido.
        const vector = await embed(text);
        const hits = await search(vector, { topK: 10 });
        if (rerank && hits.length) {
          await rerank(text, hits.slice(0, 5));
        }
        touched += hits.length;
      }
      const elapsedMs = now() - startedAt;
      logger.info('warmup.done', { reason, queries: WARMUP_QUERIES.length, touched, elapsedMs });
      return { elapsedMs, touched };
    } catch (error) {
      // Aquecimento que falha nunca derruba o servico: ele e otimizacao,
      // nao dependencia. Mas precisa aparecer no log, senao morre calado.
      logger.warn('warmup.failed', { reason, message: error.message });
      return { failed: true };
    } finally {
      running = false;
    }
  }

  function startKeepWarm(intervalMs = 60 * 1000) {
    const timer = setInterval(() => {
      // Se houve trafego real recente, o indice ja esta quente:
      // aquecer de novo so gasta CPU e polui a metrica de latencia.
      if (now() - lastRealQueryAt < IDLE_THRESHOLD_MS) return;
      warmOnce({ reason: 'ocioso' });
    }, intervalMs);
    timer.unref?.();
    return () => clearInterval(timer);
  }

  return { warmOnce, startKeepWarm, markRealQuery };
}

Dois detalhes desse código são o que separam um aquecimento útil de um cron inútil. O primeiro é a checagem de ociosidade: aquecer um serviço que está recebendo tráfego normal não melhora nada e ainda infla o percentil, porque as consultas de aquecimento entram na mesma fila das reais. O segundo é o tratamento de falha: aquecimento é otimização, então falhar nele não pode derrubar o processo, mas falhar em silêncio é pior, porque o time passa meses acreditando que existe um aquecimento que na verdade nunca rodou.

  • Use consultas reais anonimizadas, cobrindo temas diferentes, e não uma única consulta sintética.
  • Aqueça também o reranking e qualquer segundo modelo do caminho, porque eles têm carregamento próprio.
  • Repita periodicamente durante ociosidade, já que o índice esfria por paginação e não só por reinício.
  • Não aqueça quando há tráfego real recente: além de inútil, contamina a métrica de latência.
  • Revise a lista de consultas quando a base mudar de forma relevante, senão você aquece a região errada do espaço.

03

Prontidão é diferente de saúde

O maior amplificador desse problema não é o aquecimento em si, é o momento em que a réplica começa a receber tráfego. Em quase toda implantação em contêiner, o teste de prontidão responde que está pronto assim que o processo sobe uma porta HTTP. O orquestrador então roteia tráfego imediatamente e o primeiro cliente da nova réplica paga o custo integral do carregamento do índice. Como isso acontece justamente durante uma implantação ou um pico de escala, é o pior momento possível.

A correção é barata e quase sempre esquecida: separar o teste de vivacidade do teste de prontidão e fazer a prontidão depender do aquecimento concluído. O processo está vivo desde o começo, mas só se declara pronto depois que uma consulta representativa completou dentro do orçamento de latência esperado.

Linha do tempo de uma replica nova

  SEM controle de prontidao
  t0  processo sobe -------------------------------------------+
  t0  porta HTTP abre                                          |
  t0  /ready responde 200  <-- mentira: o indice esta no disco  |
  t0  orquestrador comeca a rotear                             |
  t0  cliente A chega ...... 8.2 s  <-- paga o carregamento    |
  t8  cliente B chega ...... 0.4 s                             |
                                                               v
                                          reclamacao chega sem rastro no painel

  COM controle de prontidao
  t0  processo sobe
  t0  /health (vivacidade) responde 200
  t0  /ready responde 503  <-- honesto: ainda esta aquecendo
  t0  warmup: embed -> search -> rerank, 6 consultas reais
  t6  ultima consulta de aquecimento em 0.35 s
  t6  /ready responde 200
  t6  orquestrador comeca a rotear
  t6  cliente A chega ...... 0.4 s

  O custo nao sumiu: ele mudou de dono.
  Antes pagava o cliente. Agora paga a implantacao.

Vale ser explícito sobre o que essa mudança faz e o que ela não faz. Ela não torna o carregamento mais rápido: os mesmos seis segundos continuam existindo. O que ela faz é transferir esses seis segundos da conta do cliente para a conta da implantação, que é onde o custo é aceitável porque a implantação já espera por isso. Esse é o mesmo raciocínio de qualquer transferência de custo em sistemas distribuídos, e é o motivo pelo qual a mudança compensa mesmo sem nenhum ganho de tempo total.

Há um efeito colateral a vigiar: se a prontidão depende do aquecimento e o aquecimento demora demais, o orquestrador pode interpretar a réplica como falha e reiniciá-la, criando um laço de reinícios que nunca converge. O orçamento de tempo do teste de prontidão precisa ser maior que o pior tempo de aquecimento medido, com folga, e o aquecimento precisa ter um limite superior próprio: se ele passar do teto, a réplica se declara pronta assim mesmo e registra o evento, porque uma réplica lenta serve melhor que uma réplica que nunca entra.

04

Pré-carregar na memória ou aceitar o primeiro acesso

Depois de resolver a prontidão, sobra a decisão estrutural: o índice inteiro deve morar na memória do processo, ou é aceitável que ele fique no disco e suba por demanda? Não existe resposta única, e o critério não é o tamanho absoluto do índice, é a relação entre o tamanho e a memória disponível, somada à tolerância a latência de cauda.

EstratégiaQuando faz sentidoCustoRisco principal
Índice inteiro em memóriaO índice cabe com folga e a latência de cauda é críticaMemória reservada o tempo todo, mesmo ociosoEscalar horizontalmente fica caro por réplica
Arquivo mapeado com aquecimento periódicoÍndice grande, tráfego irregular, latência toleranteCPU esporádica do aquecimentoO sistema operacional pode devolver as páginas a qualquer momento
Índice remoto gerenciadoMuitas réplicas pequenas e sem estadoLatência de rede em toda consulta, não só na primeiraO aquecimento vira problema do provedor e some do seu controle
Índice quente parcialDistribuição muito enviesada, poucos temas dominamComplexidade de manter a partição quente atualizadaConsulta fora da partição quente volta a ser lenta

A quarta linha é a mais interessante e a menos usada. Em atendimento, a distribuição de consultas é extremamente enviesada: uma fração pequena dos documentos responde à maioria esmagadora das perguntas. Manter um índice pequeno e sempre quente com esses documentos, com recurso ao índice completo quando a busca quente não atinge o limiar de similaridade, dá quase todo o benefício de manter tudo em memória com uma fração do custo. O preço é operacional: alguém precisa recalcular a partição quente periodicamente, e uma partição desatualizada silenciosamente vira uma camada que nunca acerta e só adiciona latência.

Uma armadilha comum nessa decisão é dimensionar a memória pelo tamanho do arquivo do índice. O consumo real em memória inclui o grafo de navegação, os mapas de identificadores, as tabelas de quantização e a folga que o alocador precisa durante a construção, o que costuma dar bem mais que o arquivo em disco. Dimensionar pelo arquivo é o caminho mais curto para descobrir a diferença através do processo sendo encerrado por falta de memória em produção, e um processo encerrado dessa forma reinicia frio, o que reintroduz exatamente o problema que se queria resolver.

05

Medir com percentil por estado, não com a média do dia

A razão pela qual esse problema sobrevive tanto tempo é estatística. Se um serviço responde dez mil consultas por dia e cinco delas custam oito segundos, a média sobe alguns milissegundos e o percentil noventa e cinco não se move. Até o percentil noventa e nove pode não se mover. O evento é real, é sentido pelo cliente, e é invisível em todo painel construído sobre agregação simples.

A correção é medir separando estados em vez de mudar de percentil. Marque cada consulta com o estado do processo no momento em que ela chegou e agregue por esse rótulo. A partir daí, a pergunta deixa de ser "qual a latência do serviço" e passa a ser "qual a latência da primeira consulta depois de uma janela fria", que é a pergunta que corresponde ao sintoma.

// metrics/query-state.js
// Rotula a consulta pelo estado do processo, para que a latencia fria
// nao seja diluida pela latencia quente na mesma serie.

const COLD_WINDOW_MS = 5 * 60 * 1000;

export function createQueryStateTracker({ metrics, now = () => Date.now() }) {
  const bootedAt = now();
  let lastQueryAt = null;
  let queriesSinceBoot = 0;

  function classify() {
    queriesSinceBoot += 1;
    if (queriesSinceBoot === 1) return 'first_after_boot';
    if (lastQueryAt !== null && now() - lastQueryAt > COLD_WINDOW_MS) return 'first_after_idle';
    return 'warm';
  }

  async function observe(fn) {
    const state = classify();
    const startedAt = now();
    try {
      return await fn();
    } finally {
      const elapsedMs = now() - startedAt;
      lastQueryAt = now();
      // Um unico histograma com rotulo de estado: permite ver p50 quente
      // e p50 frio lado a lado sem criar duas pipelines de metrica.
      metrics.histogram('rag.query.latency_ms', elapsedMs, {
        state,
        uptime_bucket: now() - bootedAt < 60000 ? 'lt_1m' : 'gte_1m',
      });
      if (state !== 'warm') {
        metrics.counter('rag.query.cold_start_total', 1, { state });
      }
    }
  }

  return { observe };
}

// O alerta util nao e sobre a media do servico. E este:
//   p50 de rag.query.latency_ms{state="first_after_idle"} > 2000ms
// Ele dispara com cinco eventos por dia, que e exatamente
// o volume que a media do dia apaga por completo.

Com esse rótulo, o gráfico conta uma história direta: a série quente fica plana e baixa, a série fria fica alta e esparsa, e o efeito de qualquer mudança no aquecimento aparece na série fria em minutos. Sem o rótulo, a mesma mudança produz uma variação de dois milissegundos na média, que ninguém consegue distinguir de ruído, e o time desiste de otimizar aquilo por falta de sinal.

  • Conte o número de partidas frias por dia: se ele for alto, o problema não é aquecimento, é o processo reiniciando demais.
  • Meça o tempo de aquecimento como métrica própria, porque ele cresce junto com a base e avisa antes de virar problema.
  • Correlacione partida fria com implantação e com evento de escala, para saber qual dos dois domina.
  • Alerte pelo percentil da série fria, não pelo da série agregada, que já nasce diluída.
  • Registre no log quando o aquecimento é pulado por ociosidade insuficiente, para não confundir pulo com falha.

06

A ordem de ataque que compensa

Todas as técnicas acima têm custo de implementação e nem todas valem a pena no mesmo momento. A ordem abaixo é a que dá mais resultado por unidade de esforço na maioria dos sistemas de RAG de porte médio, e cada etapa só faz sentido depois da anterior porque sem medição você não sabe se a etapa seguinte melhorou alguma coisa.

  1. Instrumente o estado da consulta antes de mudar qualquer coisa: sem a série fria separada, todo ganho posterior é invisível e toda regressão passa.
  2. Separe vivacidade de prontidão e faça a prontidão esperar o aquecimento, com teto de tempo e registro quando o teto estourar.
  3. Substitua a consulta sintética por um conjunto de consultas reais que cubram temas diferentes, incluindo o reranking na cadeia aquecida.
  4. Adicione aquecimento periódico condicionado a ociosidade, que é o que resolve o caso da primeira consulta da manhã.
  5. Só então decida sobre memória: pré-carregar tudo, manter partição quente ou aceitar o arquivo mapeado, com a série fria mostrando o efeito de cada escolha.
  6. Se o número de partidas frias por dia continuar alto, pare de otimizar aquecimento e investigue por que o processo reinicia tanto.

O último item é o que mais economiza tempo. Muito time investe semanas em aquecimento sofisticado quando a causa real é um limite de memória apertado demais que faz o contêiner ser encerrado três vezes por dia, ou uma implantação contínua que sobe réplicas novas a cada meia hora. Nesses casos o aquecimento é um curativo caro sobre um problema de dimensionamento, e a correção certa custa uma linha de configuração.

Fecha bem lembrar de onde vem o ganho real: aquecimento não deixa o sistema mais rápido, ele deixa o sistema previsível. A média já estava boa antes de tudo isso. O que muda é que o cliente que chega às sete da manhã passa a ter a mesma experiência do cliente que chega às três da tarde, e é essa consistência, não o número no painel, que determina se as pessoas confiam no atendimento.

FAQ

Perguntas frequentes

Basta rodar uma consulta no boot para aquecer o índice vetorial?

Resolve parte do problema e esconde o resto, que é o pior dos dois mundos. Uma consulta única aquece bem o modelo de embedding, porque a inferência custa praticamente o mesmo para qualquer texto, mas aquece muito mal o índice, já que a busca por vizinho aproximado só percorre a região do grafo próxima daquele vetor específico. Com um índice de milhões de vetores, você acabou de subir para a memória a vizinhança de um ponto e nada mais, então a próxima consulta real sobre outro assunto continua pagando leitura de disco. O aquecimento que funciona usa um conjunto de consultas reais anonimizadas que cubram temas diferentes, inclui o reranking na cadeia porque ele carrega um segundo modelo, e se repete durante janelas de ociosidade, já que o índice esfria por paginação do sistema operacional e não apenas por reinício do processo.

Se o aquecimento não reduz o tempo total, por que ele compensa?

Porque ele muda quem paga esse tempo. Os seis segundos de carregamento continuam existindo em qualquer cenário: a diferença é se eles caem na conta do primeiro cliente que chega ou na conta da implantação, que já é um processo assíncrono onde ninguém está esperando resposta. Fazer o teste de prontidão depender do aquecimento concluído transfere o custo para o lado certo, e o cliente deixa de encontrar oito segundos de espera sem nenhuma explicação visível no painel. O ganho, portanto, não é de velocidade média, é de previsibilidade: a diferença entre a experiência das sete da manhã e a das três da tarde desaparece, e é essa consistência que define se as pessoas confiam no canal. Vale só cuidar do orçamento de tempo do teste de prontidão, que precisa ser maior que o pior aquecimento medido para não gerar um laço de reinícios.

Como saber se vale manter o índice inteiro em memória?

O critério não é o tamanho absoluto do índice, é a relação entre o consumo real em memória e o que a réplica pode reservar, cruzada com a tolerância a latência de cauda. O erro clássico é dimensionar pelo tamanho do arquivo no disco, quando o consumo verdadeiro inclui grafo de navegação, mapas de identificadores, tabelas de quantização e a folga do alocador, o que costuma ficar bem acima do arquivo. Se o índice cabe com folga e a cauda importa, pré-carregar é a escolha simples. Se ele é grande e o tráfego é irregular, arquivo mapeado com aquecimento periódico entrega quase o mesmo com muito menos memória reservada. E quando a distribuição de consultas é bem enviesada, o que é a regra em atendimento, um índice quente parcial com os documentos que respondem a maioria das perguntas, com recurso ao índice completo abaixo do limiar de similaridade, costuma ser o melhor custo-benefício, desde que alguém mantenha essa partição atualizada.

A primeira consulta do dia é uma métrica, não um detalhe

A lentidão da primeira consulta não vem de um componente, vem de quatro caches frios independentes: o modelo de embedding, as páginas do índice no disco, as estruturas em memória do motor de busca e o modelo de reranking que quase ninguém lembra de aquecer. Consulta sintética no boot resolve só o primeiro deles, prontidão que responde pronto antes do aquecimento joga o custo no cliente, e a média do dia apaga completamente um evento que acontece cinco vezes e é sentido por cinco pessoas reais. Instrumentar o estado da consulta, separar vivacidade de prontidão, aquecer com consultas representativas, repetir durante ociosidade e só então decidir sobre memória é a sequência que transforma latência imprevisível em latência estável. Posso instrumentar o seu pipeline de RAG para separar a série fria da quente, ajustar prontidão e aquecimento e dimensionar a estratégia de memória com dado medido em vez de estimativa.