Ajuste de rendimiento de Nginx: gzip, caché y pool de conexiones

La semana pasada saltó una alerta: la página de inicio de un e-commerce tardaba 4 segundos en cargar. Abrí Chrome DevTools y vi un HTML de 120KB, más 350KB de CSS y JS sin comprimir. Peor aún: cada petición iba al backend y la tasa de aciertos de caché era del 12%. Esa noche, tras quedarme hasta tarde, dediqué 2 horas a ajustar Nginx: activé gzip, completé la estrategia de caché y retocé el pool de conexiones. A la mañana siguiente, la home ya cargaba en 1,6 segundos y el QPS del backend había bajado casi a la mitad.
Este tipo de problemas es muy común. Mucha gente instala Nginx y listo: gzip apagado por defecto, caché con dos líneas a medias y el límite de conexiones en valores por defecto. Llega el pico de tráfico y el servidor se ahoga.
En este artículo recopilo la configuración de ajuste de rendimiento de Nginx que he validado en producción. La compresión gzip reduce 60-80% del volumen de transferencia; con Brotli ahorras 15-25% más; con 95% de aciertos en caché la carga del backend cae un 90%; un pool de conexiones bien dimensionado multiplica por tres o cuatro la concurrencia; y con Thread Pools y reuseport el RPS de una sola máquina puede llegar a 50K-80K. Detallo cada módulo, los errores que he cometido y los datos medidos.
Capítulo 1: Compresión gzip/Brotli — reducir el volumen de transferencia
Primero, por qué gzip importa tanto. Imagina un HTML de 100KB: comprimido con gzip puede quedar en 20-25KB. Esos 75-80KB ahorrados significan carga más rápida para el usuario y menos coste de ancho de banda para ti.
Me di cuenta en un proyecto de un cliente. Más del 60% de usuarios eran móviles, muchos en 4G. La home tardaba 3-4 segundos y la tasa de rebote llegaba al 70%. Tras activar gzip, el volumen de transferencia cayó un 70% y el tiempo de primera pantalla bajó a unos 1,5 segundos.
1.1 Configuración básica de gzip: que funcione primero
La configuración de gzip en Nginx no es complicada; el núcleo son unas pocas líneas:
http {
gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
}
gzip on es el interruptor. gzip_vary on es clave: añade Vary: Accept-Encoding en la respuesta, avisando a CDN y navegador de que el contenido varía según la capacidad de compresión del cliente, evitando cachés incorrectas.
gzip_min_length en 1000 bytes significa que archivos menores de 1KB no se comprimen. En ficheros muy pequeños el beneficio es escaso y se gasta CPU. gzip_types define los MIME a comprimir; por defecto solo text/html, hay que añadir CSS, JS, JSON, XML, etc.
1.2 Configuración avanzada de gzip: nivel de compresión y lista de MIME
El nivel de compresión es un equilibrio. gzip_comp_level va de 1 a 9: a más número, más ratio pero más CPU.
Hice una batería de pruebas:
| Nivel | Ratio HTML | Tiempo CPU (ms) | Escenario recomendado |
|---|---|---|---|
| 1 | 65% | 2 | CPU ajustada |
| 4 | 72% | 3 | Equilibrado (recomendado) |
| 6 | 75% | 5 | Ancho de banda ajustado (recomendado) |
| 9 | 78% | 12 | Escenarios extremos |
En la práctica, los niveles 4 y 6 son los mejores en la mayoría de casos. El nivel 9 duplica el consumo de CPU por solo unos puntos más de compresión; no compensa.
Esta es mi configuración gzip completa en producción:
# Configuración de compresión gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/xhtml+xml
application/x-javascript;
gzip_disable "msie6";
gzip_proxied any suele pasarse por alto. Si Nginx actúa como proxy inverso y el backend no envía Content-Length, por defecto no comprime. Con any se fuerza la compresión de respuestas que cumplan las condiciones.
gzip_disable "msie6" evita problemas con IE6 antiguo. Hoy IE6 casi no existe; puedes quitarlo, pero yo lo dejo por precaución.
1.3 ¿Qué tipos de archivo rinden más?
Según mis mediciones, el efecto varía bastante:
| Tipo | Tamaño original | Comprimido | Ratio |
|---|---|---|---|
| HTML | 100KB | 20-25KB | 75-80% |
| CSS | 80KB | 24-28KB | 65-70% |
| JavaScript | 120KB | 36-42KB | 65-70% |
| JSON API | 50KB | 20-25KB | 50-60% |
| Imagen/vídeo | Ya comprimido | Sin sentido | 0-5% |
Imágenes y vídeos ya vienen comprimidos (JPEG, PNG, MP4); volver a gziparlos puede aumentar el tamaño. No añadas image/* ni video/* a gzip_types.
Una vez ayudé a alguien que tenía image/jpeg en gzip_types y las imágenes crecían un 3-5%. Error tonto que yo también cometí al principio, cuando quería meter todos los MIME posibles.
1.4 Brotli: 15-25% más que gzip
Si quieres ir más lejos, Brotli es mejor opción. Algoritmo de Google que, a igual nivel, comprime 15-25% más que gzip. Muy útil en recursos estáticos; el soporte en navegadores ya es amplio.
Ojo: Brotli no viene por defecto en Nginx; hay que compilar o instalar un módulo dinámico. Yo uso el módulo oficial dinámico sin recompilar Nginx:
# Cargar módulos primero (si son dinámicos)
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
http {
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;
brotli_min_length 256;
}
El nivel de Brotli va de 1 a 11; recomiendo 6. Niveles muy altos (p. ej. 11) alargan mucho el tiempo de compresión en contenido dinámico. En estáticos puedes precomprimir al máximo y servir archivos .br.
Datos comparativos:
| Método | HTML 100KB comprimido | Tiempo | Soporte navegador |
|---|---|---|---|
| gzip (nivel 6) | 25KB | 5ms | Casi total |
| Brotli (nivel 6) | 18KB | 15ms | 95%+ |
| Brotli (precompresión nivel 11) | 15KB | 0 | 95%+ |
Mi consejo: contenido dinámico con Brotli 4-6; estáticos precomprimidos. Si compilar Nginx es un lío, gzip basta: 75% de compresión ya es notable.
Capítulo 2: Estrategia de caché — acelerar contenido estático
La caché es donde el rendimiento paga más rápido. Bien configurada, el 95% de peticiones las responde Nginx sin tocar el backend. He visto sistemas con el backend al límite mientras la caché de Nginx casi no servía — no por estar apagada, sino mal configurada.
2.1 ¿proxy_cache o fastcgi_cache?
Nginx ofrece dos mecanismos:
- proxy_cache: cachea respuestas del upstream; ideal en proxy inverso (Node.js, Python, Go)
- fastcgi_cache: cachea respuestas FastCGI; ideal con PHP-FPM
Elige según tu stack. PHP → fastcgi_cache; Node.js, Python, Go → proxy_cache. La lógica es casi igual; aquí uso proxy_cache.
2.2 Configuración completa de proxy_cache
Primero define la ruta de caché en el bloque http:
http {
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=my_cache:10m
max_size=10g
inactive=60m
use_temp_path=off;
}
Línea a línea:
levels=1:2: estructura de directorios en dos niveles, evita demasiados ficheros en un solo directoriokeys_zone=my_cache:10m: nombre de zona y memoria para metadatos; 10m ≈ 80.000 clavesmax_size=10g: tope total; al superarlo se expulsa por LRUinactive=60m: entradas sin acceso en 60 minutos se eliminanuse_temp_path=off: escribe directo en caché, sin mover temporales
Luego en server o location:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_cache my_cache;
# Validez de caché
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 1m;
# Diseño de clave
proxy_cache_key $scheme$request_method$host$request_uri;
# Degradación
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
# Cabecera de estado (depuración)
add_header X-Cache-Status $upstream_cache_status;
}
}
proxy_cache_valid define cuánto cachear por código:
200 302 10m: respuestas OK, 10 minutos404 1m: 404 un minuto, evita martillar el backend con peticiones maliciosasany 1m: otros códigos, un minuto
2.3 Clave de caché e invalidación
proxy_cache_key define qué peticiones son «iguales». Por defecto es $scheme$proxy_host$request_uri; yo recomiendo declararlo explícito:
proxy_cache_key $scheme$request_method$host$request_uri;
Incluye protocolo, método, host y URI. Con GET y POST o varios dominios, es más preciso.
Invalidar caché duele. Estrategias habituales:
- Expiración temporal:
proxy_cache_valid - Bypass activo:
proxy_cache_bypass - Purga: Nginx Plus tiene
proxy_cache_purge
Suelo usar bypass por cabecera:
# Cabecera específica para saltar caché
proxy_cache_bypass $http_x_nocache;
# O parámetro de query
proxy_cache_bypass $arg_nocache;
Para refrescar: ?nocache=1 o cabecera X-Nocache: 1.
2.4 Degradación: servir aunque el backend falle
proxy_cache_use_stale es muy útil. Si el backend falla o hace timeout, Nginx puede devolver caché caducada en lugar de error.
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
El Singles’ Day del año pasado, al escalar el backend hubo 502 intermitentes en la API. Gracias a esta degradación, los usuarios casi no lo notaron: contenido un poco viejo pero usable hasta que el backend se recuperó.
Comparativa:
| Métrica | Sin caché | Acierto | Modo degradado |
|---|---|---|---|
| Tiempo respuesta | 150-200ms | 5-10ms | 5-10ms |
| QPS backend | 1000 | 50 | 0 |
| Experiencia | Normal | Normal | Algo más lenta |
Con acierto, de 200ms a 5-10ms: casi 20 veces más rápido.
2.5 Microcaché: acelerar contenido dinámico
Muchos creen que lo dinámico no se cachea; con microcaché (1-5 segundos) reduces mucho la presión en picos.
Lo probé en la home de un e-commerce: recomendaciones e inventario en tiempo real. En promoción el backend no aguantaba. Añadí microcaché de 5 segundos:
proxy_cache_path /var/cache/nginx/micro levels=1:2 keys_zone=micro:10m max_size=1g;
location / {
proxy_cache micro;
proxy_cache_valid 200 5s; # Solo 5 segundos
proxy_cache_lock on; # Evita avalancha al expirar
proxy_cache_background_update on; # Actualiza en segundo plano
}
proxy_cache_lock on es clave: al expirar, la primera petición va al backend y el resto espera la caché vieja; no hay tormenta de peticiones al backend.
El efecto sorprendió: TTFB de la home de ~800ms a ~5ms; QPS del backend de 2000 a 400. Cinco segundos de retraso casi imperceptible; la máquina respira.
2.6 Peticiones condicionales: ahorrar ancho de banda
proxy_cache_revalidate usa If-Modified-Since / If-None-Match para validar con el backend. Si responde 304 Not Modified, no retransmite el cuerpo completo.
proxy_cache_revalidate on;
Muy útil con respuestas grandes que cambian poco.
Capítulo 3: Pool de conexiones — imprescindible en alta concurrencia
gzip y caché responden a «transferir más rápido»; el pool responde a «soportar más peticiones». Por defecto, un worker de Nginx aguanta como mucho 1024 conexiones. Con tráfico real, se queda corto.
3.1 worker_connections: calcula el techo
Fórmula:
Concurrencia máxima = worker_processes × worker_connections
Servidor de 8 núcleos, worker_processes 8 (o auto), worker_connections 4096:
Concurrencia máxima = 8 × 4096 = 32768
Parece mucho, pero cada petición suele usar dos conexiones (cliente→Nginx y Nginx→backend). Peticiones concurrentes reales ≈ la mitad.
Configuración en events:
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
use epoll en Linux es el default; escribirlo aclara la intención. multi_accept on acepta varias conexiones nuevas a la vez y reduce colas en picos.
3.2 keepalive del cliente: reutilizar conexiones
Abrir TCP cuesta (tres vías). keepalive permite reutilizar la conexión cliente–Nginx.
http {
keepalive_timeout 65;
keepalive_requests 1000;
}
keepalive_timeout 65: mantiene 65 s; ni demasiado largo (recursos) ni demasiado corto (poco reuso). 60-75 s suele ir bien.
keepalive_requests 1000: máximo de peticiones por conexión. 1000 es un valor que me ha funcionado bien.
También existe keepalive_time, tiempo total de vida de la conexión:
keepalive_time 1h; # Máximo 1 hora por conexión
3.3 upstream keepalive: pool hacia el backend
Muchos no lo conocen y el efecto es claro: Nginx también reutiliza conexiones hacia el backend.
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
keepalive 64;
keepalive_timeout 60s;
keepalive_requests 1000;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
keepalive 64: pool de conexiones idle; suele ser 4-8 veces el número de backends.
proxy_http_version 1.1 y proxy_set_header Connection "" son obligatorios. Sin ellos, upstream keepalive no funciona.
Comparativa:
| Config | Conexiones/min | CPU | Escenario |
|---|---|---|---|
| Sin upstream keepalive | 6000 | Alta | Bajo tráfico |
| keepalive 32 | 3000 | Media | Tráfico medio |
| keepalive 64 | 1500 | Baja | Alto tráfico |
Con upstream keepalive, el coste de abrir conexiones cae ~50%. Crítico en alta concurrencia.
3.4 Límite de descriptores de archivo: capa del sistema
Las conexiones dependen del límite de descriptores. worker_connections 4096 con ulimit 1024 no sirve.
Comprueba:
ulimit -n
Si es menor que 65536, sube el límite en /etc/security/limits.conf:
* soft nofile 65536
* hard nofile 65536
Y en Nginx:
worker_rlimit_nofile 65536;
En el bloque main, junto a worker_processes.
3.5 Tabla rápida de parámetros recomendados
| Parámetro | Bajo (<1000 QPS) | Medio (1000-5000) | Alto (>5000) |
|---|---|---|---|
| worker_processes | auto | auto | auto |
| worker_connections | 1024 | 2048 | 4096 |
| keepalive_timeout | 60 | 65 | 75 |
| keepalive_requests | 100 | 500 | 1000 |
| upstream keepalive | 16 | 32 | 64 |
| worker_rlimit_nofile | 4096 | 8192 | 65536 |
Punto de partida; ajusta con wrk o ab mirando conexiones y latencia.
Capítulo 4: Optimización avanzada — Thread Pools y reuseport
Los tres capítulos anteriores cubren la mayoría de escenarios. Si buscas RPS >50K en una máquina o latencia muy baja, prueba esto.
4.1 Thread Pools: superar el cuello de sendfile
Nginx es event-driven en un hilo; en estáticos de mucha concurrencia, sendfile lee disco y escribe socket en el mismo worker. IO lento bloquea el worker.
Thread Pools separan lectura y envío en un pool; el worker solo coordina.
El blog oficial de Nginx documenta hasta 9× en descargas de ficheros de 1MB limitadas por disco.
http {
thread_pool default threads=32 max_queue=65536;
aio threads=default;
sendfile_max_chunk 512k;
}
threads=32, cola máxima 65536. aio threads=default activa IO async. sendfile_max_chunk 512k limita el tamaño de cada envío.
No es panacea: en APIs dinámicas el disco no es el cuello y puede empeorar por context switching. Úsalo en estáticos y descargas grandes.
4.2 Socket Sharding (reuseport): menos latencia
Desde Nginx 1.9.1, reuseport permite que varios workers escuchen el mismo puerto sin pelearse por un solo socket.
Sin reuseport comparten listen socket y compiten por accept mutex → jitter en alta concurrencia.
Con reuseport:
server {
listen 80 reuseport;
}
Cada worker tiene su socket; el kernel reparte conexiones nuevas.
Datos oficiales de Nginx:
| Métrica | Sin reuseport | Con reuseport |
|---|---|---|
| Latencia media | 15,65ms | 12,35ms |
| Desviación | 3,5ms | 1,2ms |
| Distribución | Desigual | Uniforme |
~21% menos latencia y mucho menos jitter. Más visible por encima de ~20K QPS.
4.3 open_file_cache: caché de descriptores
En sitios estáticos, cachea descriptores y metadatos:
http {
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}
max=10000: hasta 10.000 entradasinactive=30s: sin acceso en 30 s se limpiaopen_file_cache_valid 60s: revalida cada 60 sopen_file_cache_errors on: cachea también errores (p. ej. archivo inexistente)
Muy útil en estáticos; en dinámico puede servir contenido obsoleto.
Capítulo 5: Plantilla integrada — lista para producción
Tras la teoría, una plantilla unificada. Ajusta parámetros según tu caso.
# Plantilla nginx.conf — alto tráfico
user nginx;
worker_processes auto;
worker_rlimit_nofile 65536;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml
application/xml+rss application/xhtml+xml;
gzip_disable "msie6";
# Caché
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=my_cache:10m
max_size=10g
inactive=60m
use_temp_path=off;
# Cliente
keepalive_timeout 65;
keepalive_requests 1000;
keepalive_time 1h;
# open_file_cache (opcional estáticos)
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
keepalive 64;
keepalive_timeout 60s;
keepalive_requests 1000;
}
server {
listen 80 reuseport;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache my_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_key $scheme$request_method$host$request_uri;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
add_header X-Cache-Status $upstream_cache_status;
}
}
}
Configuración según escenario
E-commerce: home y ficha de producto cambian seguido; caché 10-15 minutos. upstream keepalive grande si el backend sufre con consultas SQL. Microcaché 1-3 s en promociones.
API: datos en tiempo real; proxy_cache_valid 1-5 minutos. gzip en JSON ayuda mucho; Brotli si puedes instalarlo.
Sitio estático: HTML/CSS/JS casi fijos; caché de horas. gzip rinde al máximo. open_file_cache y Thread Pools ayudan.
Comparativa de mejoras (actualizada)
| Config | Antes | Después | Mejora |
|---|---|---|---|
| Volumen HTML (gzip) | 100KB | 25KB | 75% ↓ |
| Volumen HTML (Brotli) | 100KB | 18KB | 82% ↓ |
| Tiempo API (caché) | 200ms | 8ms | 96% ↓ |
| Conexiones concurrentes | 1000 | 4000 | 4× ↑ |
| RPS (optimizado) | 10K | 50K-80K | 5-8× ↑ |
| TTFB (reuseport) | 15,65ms | 12,35ms | 21% ↓ |
Datos míos y documentación oficial; valida con pruebas de carga en tu entorno.
Capítulo 6: Problemas frecuentes y diagnóstico
Problemas que he visto a menudo y cómo resolverlos.
P: gzip no funciona, no hay Content-Encoding: gzip
Revisa:
gzip onen el bloque http correctogzip_typesincluye tu MIME- El cuerpo supera
gzip_min_length
Prueba: curl -H "Accept-Encoding: gzip" -I http://your-site.com
P: pocos aciertos de caché, X-Cache-Status mayormente MISS
Causas:
- Clave de caché mal diseñada
proxy_cache_validdemasiado corto- Backend con
Cache-Control: no-cacheoSet-Cookie
Revisa cabeceras de respuesta.
P: worker_connections insuficiente, errores 502
En el log: worker_connections are not enough → superaste el límite.
Soluciones:
- Subir
worker_connections - Revisar fugas (keepalive mal configurado)
- Más servidores con balanceo
P: ¿gzip y sendfile entran en conflicto?
No. gzip comprime el cuerpo; sendfile es la vía de transferencia de ficheros. Contenido dinámico comprimido va en memoria; estáticos precomprimidos pueden usar sendfile.
P: upstream keepalive no funciona
Causas habituales:
- Falta
proxy_http_version 1.1 - Falta
proxy_set_header Connection ""
Van en location, no en upstream.
P: reuseport da “duplicate listen options”?
reuseport solo en una línea listen por server; no repetir en otros sitios del mismo puerto sin cuidado.
P: mucha RAM, OOM frecuente
Posibles causas:
keys_zonedemasiado grande- Demasiados ficheros en caché
- Pool keepalive excesivo
Reduce parámetros o añade memoria.
Para cerrar
El núcleo del ajuste de Nginx son tres cosas: compresión, caché y pool de conexiones. Con Brotli, microcaché, Thread Pools y reuseport puedes pasar de 10K a 50K-80K RPS en una máquina.
No es un trabajo único. Orden recomendado:
- gzip primero: cambio mínimo, beneficio inmediato, ~10 minutos
- caché después: según tu negocio, en un día
- pool de conexiones: con pruebas de carga, cuando el tráfico sea estable
- avanzado al final: Thread Pools y reuseport solo si el tráfico lo exige
Tras cada cambio, mide con wrk o ab: latencia, QPS, errores. Datos, no intuición.
Lista de comprobación:
- gzip activo con MIME completos
- gzip_comp_level 4-6
- Brotli configurado (si aplica)
- proxy_cache_path con tamaño razonable
- proxy_cache_valid acorde al negocio
- microcaché en dinámico de alto tráfico
- proxy_cache_use_stale configurado
- worker_connections ≥4096
- keepalive_timeout 60-75 s
- upstream keepalive (HTTP/1.1 + Connection)
- reuseport en alto tráfico
- descriptores de archivo ≥65536
- X-Cache-Status para depuración
Eso es todo. Si tienes dudas, déjalas en comentarios y responderé cuando pueda.
FAQ
La compresión gzip no funciona y la respuesta no tiene Content-Encoding: gzip, ¿qué hago?
La tasa de aciertos de caché es baja y X-Cache-Status muestra sobre todo MISS, ¿cómo investigarlo?
worker_connections no alcanza y aparecen errores 502, ¿cómo resolverlo?
¿Cuáles son las causas habituales de que upstream keepalive no funcione?
¿Cómo elegir entre Brotli y gzip?
¿Qué parámetros recomiendas según el tráfico?
14 min de lectura · Publicado el: 15 may 2026 · Actualizado el: 21 ago 2026
Guía práctica de Nginx
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Upstream dinámico en Nginx: descubrimiento de servicios en tiempo real con Lua
Arquitectura de tres capas de upstream dinámico en OpenResty, comparación de soluciones de health check e integración con Consul, Nacos y etcd para descubrimiento de servicios en tiempo real en entornos containerizados.
Parte 5 de 6
Siguiente
Este es el artículo más reciente de la serie por ahora.



Comentarios
Inicia sesión con GitHub para dejar un comentario