168.551 requisições/dia saturando o PHP-FPM: como resolvemos com nginx srcache + Valkey via stunnel
Publicado em 31 de março de 2026

O problema: ISR cascade em escala
O portal usava Next.js com ISR (Incremental Static Regeneration) para servir um glossário com dezenas de milhares de termos. A configuração de revalidate: 60 instruía o Next.js a regenerar cada página a cada 60 segundos — o que, com 36.508 páginas no glossário, criava um ciclo contínuo e ininterrupto de chamadas para a API WordPress interna.
O padrão de acesso ao endpoint interno era assim:
GET /wp-json/api/v1/glossary?per_page=100&page=1
GET /wp-json/api/v1/glossary?per_page=100&page=2
...
GET /wp-json/api/v1/glossary?per_page=100&page=36508Com revalidate: 60, esse ciclo completo de 36.508 requisições se repetia a cada minuto. Em dias de pico, o volume chegou a 168.551 requisições por dia só para esse endpoint. Com 150–201 workers PHP-FPM disponíveis, o socket retornava Resource temporarily unavailable em menos de 60 segundos de cada ciclo. Os bursts duravam 1–2 minutos e voltavam a cada 60–90 minutos.
p-queue com concurrency 10 foi tentado antes e não resolveu: com 36k páginas e revalidate 60s, a fila nunca esvazia antes do próximo ciclo começar. O volume total de requests não cai — apenas o pico de concorrência é suavizado.
A solução: nginx srcache + Valkey via stunnel
A arquitetura de cache foi montada em três camadas, todas rodando no mesmo host (VM na OCI):
Next.js ISR → nginx (OpenResty) com srcache → stunnel (127.0.0.1:6379) → OCI Cache Valkey (TLS)
O nginx com o módulo srcache intercepta as requisições para /wp-json/ antes de chegarem ao PHP-FPM. Se a resposta estiver no Valkey, retorna diretamente — PHP não é chamado. Se for miss, o PHP é chamado, a resposta é armazenada no Valkey, e as próximas requisições idênticas são servidas do cache.
Por que stunnel?
O OCI Cache exige conexão TLS. Os módulos redis_pass e redis2_pass do OpenResty não suportam TLS nativamente — eles fazem conexão plaintext ao Redis. A solução é o stunnel rodando como proxy local: o nginx conecta em 127.0.0.1:6379 (plaintext), o stunnel encripta e repassa para o FQDN do OCI Cache na porta 6379 via TLS.
Configuração do stunnel
/etc/stunnel/redis-oci.conf
[redis-oci]
client = yes
accept = 127.0.0.1:6379
connect = <oci-cache-fqdn>.redis.sa-saopaulo-1.oci.oraclecloud.com:6379
verifyChain = noAtivando como serviço systemd persistente:
systemctl enable stunnel@redis-oci --nowTeste de conectividade antes de configurar o nginx:
redis-cli -h 127.0.0.1 -p 6379 PING # deve retornar PONGConfiguração do nginx srcache (OpenResty)
O ponto crítico é usar redis.conf em vez de wpfc.conf. A diferença fundamental:
wpfc.conf (fastcgi_cache): tem skip para $query_string — requisições com ?per_page=100&page=N NÃO são cacheadas.
redis.conf (srcache): sem skip para query string — cada ?per_page=100&page=N é cacheada individualmente. Exatamente o comportamento necessário para ISR.
O bloco de configuração srcache para /wp-json/:
location ^~ /wp-json/ {
set $key "$scheme$host$request_uri";
set $escaped_key $key; srcache_fetch_skip $skip_cache;
srcache_store_skip $skip_cache;
srcache_response_cache_control off;
srcache_fetch GET /redis-fetch $key;
srcache_store PUT /redis-store key=$escaped_key; more_set_headers 'X-SRCache-Fetch-Status $srcache_fetch_status';
more_set_headers 'X-SRCache-Store-Status $srcache_store_status'; fastcgi_pass php;
include fastcgi_params;
}O upstream Redis aponta para o stunnel local:
upstream redis {
server 127.0.0.1:6379;
keepalive 512;
}Armadilha crítica: try_files quebra tudo
A primeira versão do bloco /wp-json/ usava try_files $uri $uri/ /index.php?$args — padrão comum em configs WordPress. Isso causava redirect 301 para / em todas as requisições GET para /wp-json/: a chain era try_files → redirect interno /index.php → location = /index.php { return 301 /; } definido no redis.conf.
Nunca use try_files em locations que não servem arquivos estáticos. Para /wp-json/, use fastcgi_pass diretamente.
Armadilha: testar cache com curl -sI
Durante a validação, testar com curl -sI (HEAD request) sempre retorna X-SRCache-Store-Status: BYPASS — sem body na resposta, o srcache não armazena nada. O teste correto é com GET:
# Primeira requisição: MISS (PHP chamado, resposta armazenada)
curl -s https://portal.exemplo.com/wp-json/api/v1/glossary?per_page=100&page=1 -o /dev/null -w '%{http_code} %header{X-SRCache-Fetch-Status}'
# 200 MISS# Segunda requisição: HIT (Valkey, PHP não chamado)
# 200 HITWP Redis Object Cache no mesmo banco
O WordPress também foi configurado com Redis Object Cache (plugin) apontando para o mesmo Valkey via stunnel, usando o database 0 — o mesmo do nginx srcache.
// wp-config.php
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 );A decisão de compartilhar o database 0 foi intencional: em situações de emergência, um FLUSHDB via plugin WP Redis limpa tanto o object cache quanto o cache do nginx — comportamento desejável quando há necessidade de invalidação completa imediata.
Configuração de rede na OCI
A VM WordPress e o cluster OCI Cache estão em subnets diferentes. Foi necessário adicionar regras de ingresso na Security List do cluster cache liberando TCP 6379 a partir das subnets da VM:
# Subnet da VM (pública): 10.1.0.0/24
# Subnet do OCI Cache (privada): 10.1.1.0/24
# Regra adicionada: Ingress TCP 6379 de 10.1.0.0/24Importante: sempre use o FQDN do OCI Cache, nunca o IP privado. IPs de serviços gerenciados OCI podem mudar. O FQDN é atualizado automaticamente pelo serviço.
O resultado
Após a configuração, o cache hit rate atingiu 93% no primeiro dia. O PHP-FPM parou de saturar. As 168.551 requisições/dia continuam chegando ao nginx — mas 93% delas são respondidas diretamente pelo Valkey, sem tocar o PHP.
X-SRCache-Fetch-Status: HIT → Valkey respondeu, PHP não foi chamado
X-SRCache-Fetch-Status: MISS → primeira requisição, PHP chamado, resposta cacheada
X-SRCache-Store-Status: BYPASS → request com cookie de autenticação, não cacheado (comportamento correto)
O fix definitivo do problema é de código — aumentar o revalidate no Next.js de 60s para 3600s reduziria o volume de ~168k para ~36k requisições/dia. Mas enquanto o time de desenvolvimento implementa, a infra absorve a carga sem degradação.
Resumo da stack
OpenResty (nginx + srcache + redis2 modules) — intercepta requisições /wp-json/ antes do PHP
stunnel — proxy TLS local (127.0.0.1:6379 → OCI Cache FQDN:6379)
OCI Cache (Valkey 7.2) — cluster gerenciado Redis-compatible, $19/mês, região sa-saopaulo-1
WP Redis Object Cache — database 0, mesmo cluster, invalidação unificada
Custo total de infra de cache: $19/mês. Economia em PHP-FPM e CPU: não mensurável diretamente, mas evitou escalonamento horizontal do cluster OKE que estava sendo considerado antes da solução.
