Cambiar tema

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

Easton editorial illustration: service mesh rail yard

La semana pasada saltó una alerta: la portada de un sitio de e-commerce tardaba 4 segundos en cargar. Abrí Chrome DevTools y vi un HTML de 120 KB y CSS más JS sumando 350 KB — todo sin comprimir. Peor aún: cada petición llegaba al backend y la tasa de aciertos en caché era solo 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 portada cargaba en 1,6 segundos y el QPS del backend había bajado casi a la mitad.

Este tipo de problema es muy habitual. Mucha gente instala Nginx y lo deja correr: gzip desactivado por defecto, caché escrita a medias y límite de conexiones en valores por defecto. En picos de tráfico, el servidor se queda sin aliento.

Este artículo recoge la configuración de ajuste de rendimiento en Nginx que he validado en producción. La compresión gzip puede reducir el volumen de transferencia un 60-80 %; con 95 % de aciertos en caché la presión sobre el backend baja un 90 %; y un pool de conexiones bien configurado puede multiplicar la concurrencia por tres o cuatro. Repasaré cada módulo, los errores que he cometido y los datos medidos.

Capítulo 1: configuración gzip — reducir el volumen de transferencia

Primero, por qué gzip importa tanto. Imagina un HTML de 100 KB: comprimido con gzip puede quedar en 20-25 KB. Esos 75-80 KB ahorrados significan carga más rápida para el usuario y menor coste de tráfico para ti.

Me di cuenta en un proyecto de un cliente. Más del 60 % de usuarios eran móviles, muchos en 4G. La portada tardaba 3-4 segundos y la tasa de rebote superaba el 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: que funcione primero

La configuración gzip de Nginx no es compleja; 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 para que CDN y navegador sepan que el contenido varía según la capacidad de compresión del cliente, evitando errores de caché.

gzip_min_length en 1000 bytes significa no comprimir archivos menores de 1 KB. En archivos 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: 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 mayor número, más compresión y más CPU.

Hice una batería de pruebas:

NivelTasa HTMLTiempo CPU (ms)Escenario recomendado
165%2CPU limitada
472%3Equilibrado (recomendado)
675%5Ancho de banda limitado (recomendado)
978%12Escenario extremo

Los niveles 4 y 6 son la mejor opción en la mayoría de casos. El nivel 9 duplica el consumo de CPU por unos pocos puntos más de compresión; no compensa.

Configuración gzip completa que uso 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 se pasa por alto con facilidad. Si Nginx actúa como proxy inverso y la respuesta del backend no trae Content-Length, por defecto no comprime. Con any se fuerza la compresión de todas las respuestas que cumplan las condiciones.

gzip_disable "msie6" mantiene compatibilidad con IE6 antiguo, que tenía problemas con gzip. Hoy IE6 casi no existe; la línea se puede quitar, pero la dejo por precaución.

1.3 ¿Qué tipos de archivo rinden más?

Según mis mediciones, el efecto varía bastante por tipo:

TipoTamaño originalComprimidoTasa
HTML100KB20-25KB75-80%
CSS80KB24-28KB65-70%
JavaScript120KB36-42KB65-70%
API JSON50KB20-25KB50-60%
Imagen/vídeoYa comprimidoSin sentido0-5%
75-80%
Tasa de compresión de archivos HTML
Source: Datos medidos en producción

Imágenes y vídeos ya vienen comprimidos (JPEG, PNG, MP4); volver a gzip puede aumentar el tamaño. No añadas image/* ni video/* en gzip_types — sería contraproducente.

Una vez ayudé a diagnosticar un caso donde habían puesto image/jpeg en gzip_types y las imágenes crecían un 3-5 %. Error básico que yo también cometí al principio, cuando quería meter todos los MIME posibles.

Capítulo 2: estrategia de caché — acelerar contenido estático

La caché es el tramo con mayor retorno en optimización de rendimiento. Bien configurada, el 95 % de peticiones puede responderse desde 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 no estar activa, sino por estar mal configurada.

2.1 ¿proxy_cache o fastcgi_cache?

Nginx ofrece dos mecanismos:

  • proxy_cache: cachea respuestas del servidor upstream; para proxy inverso (Node.js, Python, Go, etc.)
  • fastcgi_cache: cachea respuestas FastCGI; para PHP-FPM

Elige según el stack. PHP → fastcgi_cache; Node.js, Python, Go → proxy_cache. La lógica es casi la misma; el ejemplo siguiente usa 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 por línea:

  • levels=1:2: niveles de directorio; 1:2 evita demasiados archivos en un solo directorio
  • keys_zone=my_cache:10m: nombre de zona y memoria para metadatos; 10m almacena unas 80 000 claves
  • max_size=10g: tamaño máximo total; al superarlo se expulsa por LRU
  • inactive=60m: entradas sin acceso en 60 minutos se eliminan
  • use_temp_path=off: escribe directo en el directorio de caché, sin coste de mover temporales

Luego actívalo 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;

        # Estrategia de degradación (más adelante)
        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 la duración por código de estado:

  • 200 302 10m: respuestas correctas, 10 minutos
  • 404 1m: errores 404, 1 minuto — evita que peticiones maliciosas martillen el backend
  • any 1m: otros códigos, 1 minuto

2.3 Diseño de clave e invalidación

proxy_cache_key define qué peticiones se consideran «iguales». Por defecto es $scheme$proxy_host$request_uri; yo recomiendo declararlo explícitamente:

proxy_cache_key $scheme$request_method$host$request_uri;

La clave incluye protocolo, método, host y URI completa. Con GET y POST o varios dominios, es más preciso.

Invalidar caché es incómodo. Estrategias habituales:

  1. Caducidad temporal: proxy_cache_valid
  2. Bypass activo: proxy_cache_bypass
  3. Purga: Nginx Plus comercial tiene proxy_cache_purge

Suelo usar la segunda, controlada por cabecera:

# Cabecera específica para saltar caché
proxy_cache_bypass $http_x_nocache;

# O parámetro en la URL
proxy_cache_bypass $arg_nocache;

Para refrescar, añade ?nocache=1 o la 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 directo.

proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;

El año pasado en el 11.11 (Singles’ Day), al escalar el backend hubo problemas y la API devolvía 502 intermitentes. Gracias a esta degradación, los usuarios apenas lo notaron — la caché estaba unos minutos vieja pero el contenido seguía visible. Al recuperarse el backend, la caché se actualizó sola.

Comparación de datos medidos:

MétricaSin cachéAciertoModo degradación
Tiempo de respuesta150-200ms5-10ms5-10ms
QPS backend1000500
ExperienciaNormalNormalAlgo más lenta
95%
Tasa de aciertos en caché
Source: Mediciones en producción

Con acierto en caché, el tiempo baja de 200 ms a 5-10 ms — casi 20 veces más rápido. El beneficio es muy visible.

Capítulo 3: pool de conexiones — imprescindible en alta concurrencia

gzip y caché responden a «cómo transferir más rápido»; el pool de conexiones a «cómo soportar más peticiones». Con valores por defecto, un worker de Nginx solo admite 1024 conexiones concurrentes. Con tráfico real, se queda corto al momento.

3.1 worker_connections: calcular el techo

Fórmula de conexiones concurrentes máximas:

Concurrencia máxima = worker_processes × worker_connections

Con 8 núcleos, worker_processes en 8 (o auto) y worker_connections en 4096:

Concurrencia máxima = 8 × 4096 = 32768

Parece mucho, pero cada petición suele usar dos conexiones (cliente → Nginx, Nginx → backend). La concurrencia útil ronda la mitad.

Configuración en el bloque events:

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

use epoll es el valor por defecto en Linux; escribirlo explícito aclara la intención. multi_accept on permite aceptar varias conexiones nuevas a la vez y reduce colas en picos.

3.2 keepalive del cliente: reutilizar conexiones

Establecer TCP cuesta (tres vías). keepalive reutiliza la conexión cliente–Nginx.

http {
    keepalive_timeout 65;
    keepalive_requests 1000;
}

keepalive_timeout 65: la conexión se mantiene 65 segundos. Demasiado alto consume recursos; demasiado bajo reduce la reutilización. 60-75 segundos es razonable.

keepalive_requests 1000: hasta 1000 peticiones por conexión. Muy bajo fuerza desconexiones frecuentes; muy alto puede favorecer fugas. 1000 es un valor estable en mis pruebas.

3.3 upstream keepalive: pool hacia el backend

Muchos no lo conocen, pero el efecto es claro. Nginx también puede reutilizar conexiones al backend y evitar abrir TCP en cada petición.

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: mantiene 64 conexiones inactivas en el pool. Ajusta según número de backends; suele ir 4-8 veces por servidor.

proxy_http_version 1.1 y proxy_set_header Connection "" son obligatorios. HTTP/1.1 soporta keepalive; vaciar Connection permite reutilizar. Sin esas dos líneas, upstream keepalive no funciona.

Comparación medida:

ConfiguraciónConexiones nuevas/minCPUEscenario
Sin upstream keepalive6000AltaBajo tráfico
keepalive 323000MediaTráfico medio
keepalive 641500BajaAlto tráfico

Con upstream keepalive, el coste de establecer conexiones cae un 50 %. En alta concurrencia es especialmente importante.

3.4 Tabla rápida de parámetros recomendados

Recomendaciones por escenario:

ParámetroBajo (<1000 QPS)Medio (1000-5000 QPS)Alto (>5000 QPS)
worker_processesautoautoauto
worker_connections102420484096
keepalive_timeout606575
keepalive_requests1005001000
upstream keepalive163264

Es punto de partida; el ajuste fino requiere pruebas de carga. Uso wrk o ab, observo curvas de conexiones y latencia y busco el óptimo.

Capítulo 4: plantilla integrada — lista para producción

Los tres capítulos anteriores explican la teoría; aquí va una plantilla unificada. Ajusta parámetros según tu caso; el esqueleto es reutilizable.

# Plantilla nginx.conf para producción

user nginx;
worker_processes auto;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # 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;
    gzip_disable "msie6";

    # Ruta de caché
    proxy_cache_path /var/cache/nginx
                     levels=1:2
                     keys_zone=my_cache:10m
                     max_size=10g
                     inactive=60m
                     use_temp_path=off;

    # Conexiones cliente
    keepalive_timeout 65;
    keepalive_requests 1000;

    # Grupo de backends
    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;
        server_name example.com;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";

            # Caché
            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;

            # Cabecera de depuración
            add_header X-Cache-Status $upstream_cache_status;
        }
    }
}

Configuración diferenciada por escenario

E-commerce: portada y ficha de producto cambian a menudo; caché corta, 10-15 minutos. upstream keepalive grande: muchas consultas a base de datos y presión en el backend.

API: alta exigencia de tiempo real; proxy_cache_valid puede ser solo 1-5 minutos. gzip en JSON ayuda mucho; no lo omitas.

Sitio estático: HTML, CSS y JS casi fijos; caché de 1 hora o más. gzip rinde al máximo en texto.

Capítulo 5: problemas frecuentes y diagnóstico

Algunos casos que he visto a menudo, con solución directa.

P: gzip no funciona; no aparece Content-Encoding: gzip

Revisa tres puntos:

  1. ¿gzip on está en el nivel correcto (bloque http)?
  2. ¿gzip_types incluye el MIME de tu respuesta?
  3. ¿El tamaño supera gzip_min_length?

Prueba con curl: curl -H "Accept-Encoding: gzip" -I http://your-site.com

P: baja tasa de aciertos; X-Cache-Status suele ser MISS

Causas habituales:

  • Clave de caché mal diseñada; cada petición parece distinta
  • proxy_cache_valid demasiado corto
  • Cabeceras del backend con Cache-Control: no-cache o Set-Cookie

Revisa cabeceras de respuesta y confirma que no prohíban la caché.

P: worker_connections insuficiente; errores 502

En el log de errores de Nginx, si ves worker_connections are not enough, la concurrencia superó el límite.

Solución:

  1. Subir worker_connections
  2. Buscar fugas de conexión (keepalive mal configurado)
  3. Añadir servidores y balanceo de carga

P: uso de memoria alto; OOM frecuente

Posibles causas:

  • keys_zone de proxy_cache_path demasiado grande
  • Demasiados archivos en caché y alto uso de mmap
  • Pool keepalive muy grande; conexiones inactivas ocupan recursos

Reduce esos parámetros o añade memoria al servidor.

Para cerrar

El núcleo del ajuste de rendimiento en Nginx son tres palancas: compresión gzip, estrategia de caché y pool de conexiones. Bien combinadas, la carga puede duplicar velocidad y la concurrencia multiplicarse por tres o cuatro.

No es un trabajo de una sola vez. Recomiendo este orden:

  1. Primero gzip: cambio mínimo, beneficio inmediato, unos diez minutos
  2. Luego caché: según el negocio, en un día
  3. Por último el pool: requiere pruebas de carga; mejor con tráfico estable

Tras cada cambio, valida con pruebas de carga. wrk o ab sirven; mira latencia, QPS y tasa de error. No ajustes a ojo — usa datos.

Lista de comprobación final:

  • gzip activo con MIME completos
  • gzip_comp_level entre 4 y 6, equilibrio CPU y compresión
  • proxy_cache_path configurado con tamaño razonable
  • proxy_cache_valid acorde al negocio
  • proxy_cache_use_stale para degradación
  • worker_connections en 4096 o superior
  • keepalive_timeout entre 60 y 75 segundos
  • upstream keepalive con HTTP/1.1 y cabecera Connection
  • X-Cache-Status en respuesta para depuración

Eso es todo. Si tienes dudas, déjalas en comentarios y responderé cuando pueda.

Flujo de ajuste de rendimiento en Nginx

Tres pasos para configurar en producción compresión gzip, estrategia de caché y optimización del pool de conexiones

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Activar compresión gzip

    Añade en el bloque http:

    • gzip on; activa la compresión
    • gzip_vary on; añade la cabecera Vary
    • gzip_comp_level 6; nivel de compresión (recomendado 4-6)
    • gzip_min_length 1000; no comprimir archivos menores de 1 KB
    • gzip_types especifica tipos MIME: text/plain text/css application/json application/javascript
  2. 2

    Step 2: Configurar caché proxy_cache

    Configuración en dos pasos:

    Paso 1: definir la ruta de caché
    • proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m

    Paso 2: activar caché en location
    • proxy_cache my_cache;
    • proxy_cache_valid 200 10m; respuestas correctas en caché 10 minutos
    • proxy_cache_use_stale error timeout http_502; estrategia de degradación
  3. 3

    Step 3: Optimizar parámetros del pool de conexiones

    Tres configuraciones clave:

    • worker_connections 4096; en el bloque events
    • keepalive_timeout 65; keepalive_requests 1000; en el bloque http
    • upstream keepalive 64; proxy_http_version 1.1; proxy_set_header Connection ""; configuración del pool hacia el backend

FAQ

¿Qué nivel de compresión gzip conviene elegir?
Se recomienda nivel 4-6. El nivel 4 ofrece 72 % de compresión y bajo consumo de CPU, ideal cuando la CPU es limitada; el nivel 6 alcanza 75 % y ahorra más ancho de banda, ideal cuando el ancho de banda es el cuello de botella. El nivel 9 duplica el consumo de CPU con una mejora marginal de compresión; no se recomienda.
¿Cuándo usar proxy_cache y cuándo fastcgi_cache?
Depende del stack backend: Node.js, Python, Go y otros servidores de aplicaciones usan proxy_cache; PHP-FPM usa fastcgi_cache. La lógica de configuración es casi idéntica; los parámetros clave son la ruta de caché, la validez y el diseño de la clave.
¿Cuánto conviene configurar worker_connections?
Según núcleos de CPU y tráfico estimado: bajo (&lt;1000 QPS) 1024; medio (1000-5000 QPS) 2048; alto (&gt;5000 QPS) 4096. Concurrencia real = worker_processes × worker_connections ÷ 2.
¿Cómo diagnosticar una baja tasa de aciertos en caché?
Tres pasos: 1. Revisar si la clave de caché es razonable y evita que cada petición sea distinta; 2. Comprobar si proxy_cache_valid es demasiado corto; 3. Verificar si el backend devuelve Cache-Control: no-cache o Set-Cookie que impiden la caché.
¿Por qué upstream keepalive exige HTTP/1.1?
HTTP/1.0 no soporta keepalive por defecto; hace falta HTTP/1.1 para reutilizar conexiones al backend. También se necesita proxy_set_header Connection "" para vaciar la cabecera Connection; si no, Nginx cierra la conexión y upstream keepalive no funciona.
¿Cómo fijar el tiempo de caché según el tipo de negocio?
En e-commerce, portada y ficha de producto cambian con frecuencia: 10-15 minutos. En API con alta exigencia de tiempo real: 1-5 minutos. En sitios estáticos, HTML/CSS/JS casi no cambian: más de 1 hora. Principio: cuanto más rápido cambia el negocio, más corta la caché.

11 min de lectura · Publicado el: 11 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog