Resposta rápida: configuração mínima de cache HTTP no Nginx para WordPress
Para acelerar um WordPress no Nginx sem quebrar área logada nem WooCommerce, combine três camadas: (1) expires para assets estáticos no navegador, (2) fastcgi_cache no location que repassa PHP ao PHP-FPM, e (3) fastcgi_cache_bypass condicionado a cookies wordpress_logged_in_, comment_author_, wp-postpass_ e ao método POST. Exponha $upstream_cache_status via add_header X-Cache-Status e valide com curl -I. Esse é o caminho mais estável para sites que servem PHP-FPM diretamente — a maioria das instalações WordPress em VPS.
proxy_cache vs fastcgi_cache: qual escolher
A diferença é o módulo Nginx que entrega a resposta dinâmica. proxy_cache (do ngx_http_proxy_module) cacheia respostas vindas de um upstream HTTP — útil quando o Nginx atua como reverse proxy diante de Apache, Node, Varnish ou outro Nginx. fastcgi_cache (do ngx_http_fastcgi_module) cacheia respostas que o próprio Nginx busca no PHP-FPM via FastCGI, que é o caminho mais comum em WordPress.

| Critério | proxy_cache | fastcgi_cache |
|---|---|---|
| Módulo | ngx_http_proxy_module | ngx_http_fastcgi_module |
| Quando usar | Nginx como reverse proxy diante de outro HTTP | Nginx falando direto com PHP-FPM |
| Diretivas-chave | proxy_cache_path, proxy_cache_key, proxy_cache_valid, proxy_cache_bypass | fastcgi_cache_path, fastcgi_cache_key, fastcgi_cache_valid, fastcgi_cache_bypass |
| Cenário típico WordPress | Nginx → Apache/LiteSpeed | Nginx → PHP-FPM (stack LEMP padrão) |
Regra prática: se o seu location ~ \.php$ usa fastcgi_pass, vá de fastcgi_cache. Se usa proxy_pass, vá de proxy_cache.
Headers do navegador: Cache-Control e Expires com a diretiva expires
Para CSS, JS, fontes e imagens, a diretiva expires do ngx_http_headers_module resolve em uma linha: ela emite automaticamente o header Expires e o Cache-Control: max-age=N equivalente. Segundo a documentação oficial do Nginx, expires max gera Expires apontando para 31 de dezembro de 2037 e Cache-Control: max-age=315360000 — efetivamente 10 anos.

location ~* \.(css|js|woff2|svg|jpg|jpeg|png|webp|avif)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
location ~* \.(ico|gif)$ {
expires max;
}
Detalhe que muitos tutoriais ignoram: quando Cache-Control: max-age e Expires coexistem na resposta, a RFC 9111 (HTTP Caching) determina que max-age prevalece para clientes HTTP/1.1. Expires só sobra como fallback para clientes muito antigos. Para assets versionados (com hash na URL, gerados por build), o expires longo é seguro porque mudanças no arquivo geram nova URL. Em conjunto com otimizar imagens do WordPress, esse bloco resolve a maior parte do peso do front-end.
Configurando fastcgi_cache passo a passo
Fluxo de 5 passos: (1) confirme que o binário tem ngx_http_fastcgi_module com nginx -V 2>&1 | tr ' ' '\n' | grep fastcgi; (2) declare o fastcgi_cache_path no contexto http {}; (3) ative fastcgi_cache no location PHP; (4) adicione regras de bypass; (5) exponha X-Cache-Status e teste.

Bloco completo no nginx.conf (contexto http):
fastcgi_cache_path /var/cache/nginx/wp levels=1:2 keys_zone=WORDPRESS:100m
inactive=60m max_size=1g use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_lock on;
No server {} do site:
set $skip_cache 0;
# Métodos não-GET nunca cacheiam
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
# URIs administrativas e dinâmicas do WordPress/WooCommerce
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/cart|/checkout|/my-account|sitemap(_index)?\.xml") {
set $skip_cache 1;
}
# Cookies de sessão: logados, comentaristas, posts protegidos, carrinho
if ($http_cookie ~* "wordpress_logged_in_|comment_author_|wp-postpass_|woocommerce_items_in_cart|wp_woocommerce_session_") {
set $skip_cache 1;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache-Status $upstream_cache_status always;
}
O keys_zone=WORDPRESS:100m reserva 100 MB de memória compartilhada para metadados (chega para centenas de milhares de chaves). O max_size=1g é o teto em disco. inactive=60m remove entradas não acessadas em 60 minutos, independente do TTL.
Bypass obrigatório: cookies de login, comentários, wp-postpass e POST
O guia oficial de content caching do NGINX recomenda explicitamente usar fastcgi_cache_bypass/proxy_cache_bypass baseados em cookies e argumentos para não cachear conteúdo de autenticados. Em WordPress, os cookies-gatilho são consistentes há anos:

wordpress_logged_in_*— usuário autenticado no wp-admin ou front-end.comment_author_*— visitante que comentou (vê o nome pré-preenchido).wp-postpass_*— post protegido por senha já desbloqueado.woocommerce_items_in_cart/wp_woocommerce_session_*— carrinho ativo no WooCommerce.
Para evitar if em série, dá para usar map no contexto http:
map $http_cookie $wp_skip_cookie {
default 0;
"~*wordpress_logged_in_" 1;
"~*comment_author_" 1;
"~*wp-postpass_" 1;
"~*woocommerce_items_in_cart" 1;
"~*wp_woocommerce_session_" 1;
}
map $request_method $wp_skip_method {
default 0;
POST 1;
}
E no server: set $skip_cache "${wp_skip_cookie}${wp_skip_method}"; — qualquer valor diferente de 00 ativa o bypass. Esse mesmo princípio aparece no guia de cache HTTP no Nginx para WordPress sem quebrar login, que complementa cenários adicionais de área logada.
Validando HIT/MISS com curl -I e X-Cache-Status
A variável $upstream_cache_status, definida no ngx_http_upstream_module, expõe os estados MISS, HIT, BYPASS, EXPIRED, STALE, UPDATING e REVALIDATED. Com o add_header X-Cache-Status ativo, a validação é direta:

$ curl -I https://seusite.com/blog/post-exemplo/
HTTP/2 200
server: nginx
content-type: text/html; charset=UTF-8
x-cache-status: MISS
$ curl -I https://seusite.com/blog/post-exemplo/
HTTP/2 200
x-cache-status: HIT
Se a segunda requisição ainda vier MISS, alguma regra de bypass está disparando — geralmente um cookie de sessão deixado pelo navegador. Teste de uma janela anônima, ou direto via curl sem -b. Para confirmar bypass de logado: curl -I -H 'Cookie: wordpress_logged_in_abc=1' ... deve retornar X-Cache-Status: BYPASS.
Invalidação: TTL, ngx_cache_purge e estratégias sem purge nativo
O Nginx open source não tem purge nativo. As três rotas práticas:

- TTL curto + stale-while-revalidate:
fastcgi_cache_valid 200 10m;comfastcgi_cache_use_stale updatingentrega conteúdo levemente velho enquanto revalida. Aceitável para blogs. - Módulo ngx_cache_purge (FRiCKLE): adiciona a diretiva
fastcgi_cache_purge, acionada por umlocationinterno restrito. Requer compilar Nginx com o módulo ou usar pacote que já o inclua. - Nginx Plus: oferece API de purge oficial — caminho corporativo.
Exemplo de purge com ngx_cache_purge:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}
Esse endpoint pode ser chamado por um plugin WordPress no hook save_post ou por script disparado pós-deploy. Para automatizar pelo terminal, WP-CLI para automação permite encadear o purge a comandos de publicação. Atenção: o repositório do ngx_cache_purge tem manutenção intermitente — valide compatibilidade com sua versão do Nginx antes de adotar como única estratégia.
Composição com CDN (Cloudflare/BunnyCDN) e plugins WordPress
Cache no Nginx e CDN de borda atuam em camadas diferentes e somam. Conforme a documentação da Cloudflare sobre Cache-Control, o edge respeita os headers de origem para definir TTL — ou seja, o que o seu Nginx emite no Cache-Control dos assets estáticos é o que a Cloudflare/BunnyCDN vai honrar. Recomendações de coexistência:
- Assets estáticos: TTL longo no Nginx (
expires 30doumax) e deixe o CDN espelhar. - HTML: prefira cachear apenas no Nginx (fastcgi_cache) e deixar o CDN em modo bypass cookie para o HTML, evitando servir página de logado a anônimo.
- Plugins (WP Rocket, W3 Total Cache, LiteSpeed Cache): se você já tem
fastcgi_cachebem configurado, plugins de page cache se tornam redundantes — mantenha-os só para minificação e lazy-load. Rodar dois page caches gera incoerência de invalidação.
A escolha entre Cloudflare ou BunnyCDN para WordPress impacta como você dispara purge no CDN após o purge local.
Erros comuns que quebram WordPress com cache no Nginx
- Cachear wp-admin ou wp-login.php: usuário entra com nonce de outra sessão, dashboard quebra. Sempre inclua essas URIs no
skip_cache. - Cachear AJAX (admin-ajax.php, wp-json/): nonces ficam congelados e endpoints REST retornam dados de outro usuário. Bypass obrigatório.
- Não bypassar POST: formulários de contato e checkout passam a retornar a página de confirmação de outro visitante.
- Cachear com cookie de sessão: o WooCommerce gera
wp_woocommerce_session_*em qualquer visita ao carrinho — sem bypass, o cache serve carrinho alheio. - Esquecer
fastcgi_no_cache:fastcgi_cache_bypasssozinho lê do cache mas grava; semfastcgi_no_cache, respostas de logados poluem o cache. - TTL maior que a frequência de publicação: sem purge, post novo demora a aparecer. Ajuste
fastcgi_cache_validao seu ritmo editorial. - add_header sem
always: o headerX-Cache-Statussome em respostas 4xx/5xx, dificultando debug. Useadd_header ... always;.
FAQ
Posso usar fastcgi_cache junto com WP Rocket?
Tecnicamente sim, mas é redundante. Se o Nginx já cacheia HTML, desative o page cache do WP Rocket e mantenha apenas otimizações de assets (minify, lazy-load, defer).
Quanto tempo de TTL é seguro para HTML?
Entre 10 minutos e 1 hora para sites de conteúdo. Para e-commerce, prefira TTL curto (1–5 min) ou bypass completo de páginas com preço.
O cache do Nginx funciona com HTTPS?
Sim. O TLS termina no Nginx; o cache opera no nível da resposta HTTP, sem relação com a camada de transporte.
Preciso reiniciar o Nginx para limpar o cache?
Não. Basta apagar o diretório do fastcgi_cache_path (rm -rf /var/cache/nginx/wp/*) e o Nginx recria sob demanda. Reload (nginx -s reload) não é necessário só para invalidar.