API REST com spikes de latência por queries N+1 e GC no PHP — como diagnosticar e corrigir
Por Kevin Marcondes · Publicado em · 7 min de leitura
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 conversarVeja também
- Como recuperar estatísticas e índices corrompidos no PostgreSQL que deixaram consultas lentas
- Erro 'Too many connections' no PostgreSQL durante pico de faturamento — guia prático de diagnóstico e correção
- Reduzir custos migrando Magento para OpenMage com plano de rollback e testes de aceitação
- Guia prático: como recuperar uma tabela excluída no PostgreSQL usando point-in-time recovery sem downtime