Voltar
Como reduzimos a memória do Next.js de ~5GB para ~1GB: o timer do react-query que segurava cada requisição SSR

Como a Zepto reduziu a memória dos pods Next.js de ~5 GB para ~1,2 GB ao conter um timer do react-query

Aprofundamento CEVIU

Aprofundamento

O problema da Zepto não era um vazamento clássico. No SSR, o react-query v3 cria uma Query para cada useQuery renderizado e dispara na hora um timer de expiração de cache (Query.scheduleGc), de 2 a 5 minutos. No navegador isso faz sentido, porque o timer é cancelado quando um componente assina a query. No servidor a assinatura acontece dentro de useEffect, que não roda durante o SSR. O timer corre até o fim, e o QueryClient que ele limparia já foi descartado.

O estrago vem do AsyncLocalStorage. O Node copia o store ativo para cada timer, e o Next.js renderiza cada requisição dentro de um store que referencia request, response e spans do OpenTelemetry. Timer pendente é raiz de GC, então cada requisição finalizada ficava presa até o disparo. A correção foi um cache de servidor que monta todas as queries com cacheTime: Infinity, o que impede o timer de começar. Os pods caíram de cerca de 4,8 GiB para cerca de 1,2 GiB. Na v4 e na v5 o padrão no servidor já é infinito, mas um gcTime finito explícito nos hooks reabre a armadilha. A issue TanStack/query#11320 estava sem correção mergeada em novembro de 2026.

Por que isso importa

Para quem opera Next.js em produção, a lição vale além do react-query: qualquer setTimeout criado durante uma requisição segura essa requisição até disparar. Bibliotecas feitas para o navegador fazem isso o tempo todo, com polling, debounce e retries. Se você usa gcTime próprio nos hooks, atualizar para a v5 não resolve sozinho. E se o autoscaling depende de memória, a correção pode reduzir réplicas e subir latência, então acompanhe esses números depois.

Perguntas frequentes

Por que o react-query mantinha requisições na memória no servidor?

Cada query renderizada no SSR iniciava um timer de cache de 2 a 5 minutos. O Node associa o contexto de AsyncLocalStorage da requisição a esse timer, e timers pendentes são raízes de GC. Assim a requisição inteira, com response e buffers, ficava viva até o timer disparar.

Atualizar para o react-query v5 resolve o problema?

Só em parte. Na v4 e na v5 o padrão de cacheTime/gcTime no servidor é Infinity. Mas se os hooks passam um gcTime finito explícito, o timer real volta a rodar no SSR. Nesse caso é preciso um guard no QueryClient do servidor.

Como a Zepto corrigiu?

Criou um cache para o QueryClient do servidor que monta toda query com cacheTime: Infinity, então o timer nunca começa. O valor não é desfeito pelos hooks, porque o react-query mantém o maior entre o atual e o novo. O navegador continua com o cache padrão.

Por que o autoscaling foi afetado?

Os pods escalavam por CPU e memória, e o vazamento fazia a memória cruzar o limite primeiro. Sem ele, só a CPU disparava o escalonamento, raramente atingida num servidor Node. A frota encolheu e a latência subiu, então a Zepto passou a usar CPU como sinal principal e manteve um limite de memória mais alto como segurança.

Fontes

Avalie este artigo:
Categoria
CEVIU Web Dev
Publicado
12 de novembro de 2026
Editoria
CEVIU Web Dev

Quer receber mais sobre CEVIU Web Dev?

Conteúdo curado diariamente, direto no seu e-mail.

Conteúdo curado diariamenteDiversas categoriasCancele quando quiser