API REST com spikes de latência por queries N+1 e GC no PHP — como diagnosticar e corrigir

Por · Publicado em · 7 min de leitura

Painel de instrumentos do carro com velocímetro e tacômetro iluminados em vermelho
Metáfora visual para picos de latência e monitoramento de desempenho Foto: Erik Mclean / Pexels

API REST com spikes de latência por queries N+1 e GC no PHP: diagnóstico e correções. Se seus endpoints apresentam picos intermitentes de p95/p99, este guia mostra como reconhecer os sintomas, provar o N+1, medir o impacto do GC e aplicar correções rápidas no código, no banco e no PHP‑FPM — sempre validando com métricas e tracing.

Sintomas típicos: como reconhecer os picos em produção

Spikes surgem em horários aleatórios, sem tráfego proporcional. O p95/p99 sobe, a média quase não muda. Endpoints de listagem e relatórios sofrem mais.

Nos logs, aparecem dezenas ou centenas de SELECTs parecidos em uma requisição. No PHP‑FPM, alguns workers ficam bem mais lentos e reciclam por memória.

O banco exibe rajadas curtas de QPS com padrões repetidos. O CPU do app não satura, mas há pausas no tempo de resposta — típico de queries N+1 combinadas com GC.

  • p95/p99 serrilhado e imprevisível
  • Muitas consultas semelhantes por requisição
  • Workers do PHP‑FPM “inchando” e sendo reciclados
  • Listagens/relacionamentos mais afetados

Por que N+1 e GC geram latência intermitente

N+1 ocorre quando o código busca uma coleção e, para cada item, dispara outra consulta. Com lazy loading em ORM, isso aparece sem perceber e multiplica o I/O.

O GC do PHP roda dentro da requisição. Objetos e arrays grandes criam ciclos de referência. Em certos pontos, o coletor pausa o processo para varrer estruturas.

Quando se combinam, loops criam muitos objetos, cada iteração faz I/O e, em algum momento, o GC entra. A requisição alonga e vira pico no p95/p99.

Diagnóstico observável: logs, correlação, tracing e slowlog

Torne requisições rastreáveis de ponta a ponta. Use logs estruturados com correlation ID, contadores de queries e tempo de serialização. Veja como implementar em logs estruturados e correlação.

Implemente tracing nas rotas críticas para decompor o tempo em spans de aplicação, SQL e rede. Ajuda a flagrar o momento do N+1 e pausas de GC. Guia: tracing distribuído com OpenTelemetry em PHP.

Ative o slowlog do PHP‑FPM para capturar stacks acima de um limiar e revisar gargalos em código. No banco, colete EXPLAIN das consultas mais repetidas.

  • Logs estruturados e correlação de IDs
  • Tracing com spans para app, DB e serialização
  • Slowlog do PHP‑FPM com request_slowlog_timeout
  • Amostras de EXPLAIN de consultas “quentes”

Como flagrar o N+1 de forma inequívoca

Conte queries por requisição e agrupe por fingerprint do SQL. Se a mesma consulta roda centenas de vezes em um endpoint, é N+1.

No ORM, procure lazy loading em loops. Em frameworks como Laravel, habilite eager loading nas relações afetadas e verifique a queda de consultas.

Valide com tracing: em vez de muitos spans curtos de banco, você quer poucas consultas bem planejadas (com índices).

  • Métrica: queries por request por endpoint
  • Fingerprints de SQL repetidos na mesma request
  • Eager loading/joins nas relações usadas em loops
  • EXPLAIN para planos e índices adequados

Medindo e mitigando o impacto do GC no PHP

Monitore memória por requisição e identifique pontos com crescimento abrupto. Objetos temporários grandes e arrays aninhados costumam acionar o GC.

Reduza ciclos: evite referências cruzadas desnecessárias, libere estruturas grandes cedo (unset) e considere generators para processar streams sem materializar tudo.

Em blocos específicos e medidos, desativar e reativar o GC pode reduzir pausas. Para aprofundar na estabilidade de memória do runtime, veja vazamento de memória no PHP‑FPM.

  • Liberar buffers grandes cedo
  • Evitar criar ciclos de referência
  • Usar generators/yield para pipelines grandes
  • Reciclar workers com pm.max_requests

Correções práticas e quick wins: do código ao banco

Ataque o N+1 com reescritas focadas: substitua loops com consultas por joins e eager loading. Em APIs muito acessadas, cacheie resultados quentes com TTL curto e invalidação por chave de negócio.

Otimize consultas reincidentes com índices e redução de colunas carregadas. No serializador, evite mapear objetos gigantes sem necessidade. Veja abordagens gerais em como reduzir latência de APIs REST. Valide cada mudança monitorando p95/p99 e contagem de queries.

Causa Sintoma observável Correção rápida Como validar
N+1 em relação 1:N Centenas de SELECT iguais por request Eager loading/joins; buscar IDs em lote Queries por request caem; p95 estabiliza
Campos desnecessários Payload grande e serialização lenta Selecionar apenas colunas usadas (SELECT projetado) Tempo de serialização cai; resposta menor
GC em arrays massivos Pausas em loops; alto uso de memória Processar em chunks/generators; liberar buffers Memória por request cai; pausas somem
Worker instável Latência cresce ao longo do tempo Ajustar pm.max_requests; revisar memory_limit Workers reciclam antes de “inchar”; p99 baixa

Ajustes no PHP‑FPM para previsibilidade de latência

Dimensione pm.max_children para CPU e banco, evitando sobrecarga de conexões. Defina request_terminate_timeout e slowlog para capturar travamentos.

Use pm.max_requests para reciclar processos após N requisições, reduzindo fragmentação e efeitos cumulativos de memória. Monitore a taxa de reciclagem para não cair em thrash.

Exporte o status do FPM (pm.status_path) e crie gráficos de fila, tempo médio, p95/p99 e memória. Cruze com métricas do banco para enxergar backpressure.

  • pm=dynamic ou static conforme perfil de carga
  • request_slowlog_timeout e slowlog configurados
  • pm.max_requests calibrado ao padrão de memória
  • pm.status_path exportado e monitorado

Validação e monitoramento contínuo após as correções

Defina SLOs de latência por endpoint e acompanhe histogramas (p50/p95/p99). Comite painéis e alertas no repositório para rastreabilidade.

Faça rollout gradual e compare antes/depois com tracing e logs. Se necessário, use flags para alternar versões de consultas ou serializers com segurança.

Revise periodicamente os endpoints com maior custo por requisição e reavalie índices quando padrões de acesso mudarem. Para SQL, veja também otimização de consultas PostgreSQL.

  • SLOs por rota e alertas de p99 sustentado
  • Comparação A/B por janela de tempo
  • Revisão periódica de endpoints mais caros

Perguntas frequentes

Como diferenciar N+1 de falta de índice no banco?

No N+1, a mesma consulta aparece dezenas de vezes por requisição. Com falta de índice, há poucas consultas, porém lentas. Conte queries por request, agrupe por fingerprint e rode EXPLAIN para checar se há full scan ou plano ruim.

Desabilitar o garbage collector do PHP resolve os spikes?

Pode reduzir pausas em trechos específicos e medidos, mas não é solução geral. O GC coleta ciclos; desabilitá-lo globalmente pode elevar memória e causar falhas. Prefira otimizar alocação e reciclar workers com pm.max_requests.

pm.max_requests ajuda na estabilidade de latência?

Ajuda. Reciclar processos após N requisições reduz fragmentação e efeitos cumulativos de memória. Evite reciclagem excessiva. Monitore p95/p99 e memória por worker para calibrar o valor.

Como provar que corrigi o N+1?

Antes/depois devem mostrar queda nas consultas por requisição e estabilização do p95/p99. O tracing exibirá menos spans de banco. Logs estruturados devem registrar contador de queries por rota com tendência sustentada de baixa.

Cachear a resposta JSON inteira resolve o problema?

Cache pode aliviar picos, mas não substitui a correção. Use TTL curto e invalidação por chave de negócio. Corrija o N+1 na origem (eager loading, joins) e mantenha o cache como otimização complementar.

Quer estabilizar a latência da sua API REST?

Se você enfrenta spikes por queries N+1 e pausas de GC no PHP, podemos analisar seus endpoints críticos, medir causas e propor correções práticas.

Vamos conversar

Veja também