NGINX tuning “de verdad”: lo que mantener, lo que ajustar y lo que evitar en producción

TL;DR (visión rápida)

  • No copies valores extremos de un Gist de pruebas a producción (p. ej., keepalive_requests 100000, timeouts de minutos o buffers gigantes).
  • Ajusta por evidencia (tests + métricas) y por caso de uso (estático, API, proxy, TLS/HTTP2/3).
  • Ten en cuenta la capa delante de NGINX (CDN/WAF/LB) para timeouts y real IP; configura rate limiting con X-Forwarded-For si hay proxy.
  • Límites del sistema (FDs, nofile) y systemd importan más que 20 “micro-tweaks”.

Principales aciertos del Gist (y cómo aterrizarlos)

  • worker_processes auto; ✅ perfecto hoy en día.
  • worker_connections alto ✅ , pero mide: 4.000 puede estar bien para APIs sin TLS pesado; con TLS/HTTP2 no subas a ciegas.
  • sendfile on; tcp_nopush on; tcp_nodelay on; ✅ buenos defaults (estático).
  • open_file_cache ✅ útil en sitios estáticos con muchos ficheros; prueba y monitoriza “cache hit/miss”.
  • Rate limiting (limit_req, limit_conn) ✅ pero atrás de proxy debes leer X-Forwarded-For/proxy_protocol (ver abajo).

Cosas que en producción conviene moderar

  • keepalive_requests 100000; ❌ demasiado. Usa 100–1000 según patrón.
  • send_timeout 2; ❌ agresivo (corta clientes lentos). Empieza en 10–15s.
  • client_*_timeout en minutos ❌ (para cargas normales). Mantén 15–60s según caso.
  • Buffers enormes (client_header_buffer_size 3m) ❌ pueden abrir DoS de memoria. Mantén defaults salvo cabeceras realmente grandes.

Matices “que casi nadie cuenta”

  • Con Cloudflare plan free (y muchos ALB), el backend tiene ≈100s antes de 524; no uses timeouts superiores.
  • Gzip on-the-fly aumenta CPU; mejor gzip_static on; (precomprimir) o Brotli/Zstd si tienes módulos.
  • HTTP/2 casi siempre sí; HTTP/3/QUIC opcional: activa si tu tráfico móvil/global lo necesita y tu LB lo soporta.
  • BBR (kernel) puede mejorar latencia/TTFB para conexiones de alto RTT; pruébalo, no lo fuerces.

Plantilla base “production-safe” (servidor web/proxy TLS)

Sustituye rutas, dominios y upstreams. Comenta/activa HTTP/3 si procede.

# /etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;
worker_rlimit_nofile  200000;

error_log  /var/log/nginx/error.log warn;
pid        /run/nginx.pid;

events {
    worker_connections  4096;
    # Linux moderno: epoll por defecto
    # Optimiza aceptación en escenarios de alta concurrencia
    multi_accept on;
    # En kernels nuevos puedes usar SO_REUSEPORT (por bloque 'listen reuseport' en server)
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # Logs: acceso bufferizado; desactívalo sólo en pruebas sintéticas
    access_log    /var/log/nginx/access.log main buffer=32k flush=1s;

    # Rendimiento básico E/S
    sendfile      on;
    tcp_nopush    on;   # cabeceras en un solo paquete
    tcp_nodelay   on;

    # Keepalive sensato
    keepalive_timeout   15s;
    keepalive_requests  500;

    # Timeouts conservadores
    client_body_timeout   30s;
    client_header_timeout 15s;
    send_timeout          15s;
    reset_timedout_connection on;

    # Cache de descriptores de archivo
    open_file_cache          max=100000 inactive=20s;
    open_file_cache_valid    30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors   on;

    # Compresión (usa estática si puedes)
    gzip on;
    gzip_comp_level  2;
    gzip_min_length  1024;
    gzip_vary        on;
    gzip_disable     "msie6";
    gzip_types
        text/plain text/css text/xml application/xml
        application/json application/javascript application/rss+xml
        image/svg+xml;
    # gzip_static on;  # si precomprimes .gz en build

    # Seguridad básica
    server_tokens off;

    # Real IP si hay LB/CDN delante
    # set_real_ip_from  10.0.0.0/8;    # rangos de tu LB
    # real_ip_header    X-Forwarded-For;
    # real_ip_recursive on;

    # Limitar abusos (lee IP real si hay proxy)
    limit_conn_zone  $binary_remote_addr zone=conn_ip:10m;
    limit_req_zone   $binary_remote_addr zone=req_ip:10m rate=10r/s;

    # TLS perfil moderno (ajusta a tu parque)
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:HIGH:!aNULL:!MD5';
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    # OCSP stapling si tienes chain y DNS correcto
    # ssl_stapling on; ssl_stapling_verify on;

    # HTTP/2 (HTTP/3 opcional en server)
    # ssl_ecdh_curve X25519:secp384r1;

    # Servidor de ejemplo estático
    server {
        listen 80;
        listen 443 ssl http2;   # añade http3 si procede: 'http3 reuseport'
        server_name  ejemplo.tld;

        # Certs
        ssl_certificate      /etc/ssl/certs/fullchain.pem;
        ssl_certificate_key  /etc/ssl/private/privkey.pem;

        # Límites por IP (ajusta a tu caso)
        limit_conn conn_ip 20;
        limit_req  zone=req_ip burst=20 nodelay;

        # Archivos estáticos
        root /var/www/html;
        index index.html;

        location / {
            try_files $uri $uri/ =404;
        }

        # Cabeceras de seguridad (ajusta CSP a tu app)
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header Referrer-Policy no-referrer-when-downgrade;
        add_header Permissions-Policy "geolocation=(), microphone=()";
        # add_header Content-Security-Policy "default-src 'self';" always;

        # Buffering de respuestas ascendentes (proxy) – descomenta si haces reverse proxy
        # proxy_buffering on;
        # proxy_buffers 16 16k;
        # proxy_busy_buffers_size 32k;

        # HTTP/3 (QUIC) opcional (requiere nginx quic build + firewall UDP 443)
        # listen 443 quic reuseport;
        # add_header Alt-Svc 'h3=":443"; ma=86400' always;
    }
}
Lenguaje del código: PHP (php)

¿Reverse proxy/API? añade bloque upstream, proxy_set_header Host $host;, proxy_set_header X-Real-IP $remote_addr;, proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;, proxy_http_version 1.1; y proxy_read_timeout/send_timeout acordes a tu backend (y a tu CDN/LB).


Rate limiting detrás de un proxy/CDN

Si tienes CDN o LB, no uses $binary_remote_addr (verás la IP del proxy). Configura la IP real:

set_real_ip_from  103.21.244.0/22;   # rangos CF/LB
set_real_ip_from  103.22.200.0/22;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

map $http_x_forwarded_for $client_ip {
    ~^(?P<addr>\d+\.\d+\.\d+\.\d+) $addr;
    default $remote_addr;
}
limit_req_zone $client_ip zone=req_ip:10m rate=10r/s;
limit_conn_zone $client_ip zone=conn_ip:10m;
Lenguaje del código: PHP (php)

Límites de ficheros (FD) y systemd (Linux)

Aumenta nofile vía systemd, no solo en nginx.conf:

sudo mkdir -p /etc/systemd/system/nginx.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=300000
EOF
sudo systemctl daemon-reload
sudo systemctl restart nginx
Lenguaje del código: JavaScript (javascript)

SELinux: si necesitas setrlimit, setsebool -P httpd_setrlimit 1.


HTTP/2, HTTP/3 y TLS

  • HTTP/2: activa en listen 443 ssl http2;.
  • HTTP/3/QUIC: compila NGINX con QUIC o usa build que lo incluya; abre UDP/443 y añade Alt-Svc.
  • TLS: usa TLS1.2/1.3, desactiva session_tickets, usa curvas modernas. Revisa tu parque (clientes antiguos) antes de endurecer al máximo.

Brotli / Zstd vs Gzip

  • Producción: precomprime estáticos (gzip_static on; o Brotli estático) en build; reduce CPU y mejora TTFB.
  • Dinámico: si comprimes on-the-fly, usa niveles bajos (1–2) y mide CPU. Brotli nivel 3–5 puede ser rentable en HTML/CSS/JS.

BBR (kernel) y colas

  • Habilítalo si tu tráfico es global/móvil (alto RTT):
modprobe tcp_bbr
echo 'tcp_bbr' | sudo tee /etc/modules-load.d/bbr.conf
cat <<'EOF' | sudo tee /etc/sysctl.d/99-bbr.conf
net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq
EOF
sudo sysctl --system
Lenguaje del código: PHP (php)

Checklist de no-hacer en producción

  • keepalive_requests 100000 y timeouts de minutos “porque sí”.
  • ❌ Buffers gigantes “por si acaso”: abres superficie DoS de memoria.
  • ❌ Rate limiting con IP del proxy (sin real_ip_*).
  • ❌ Desactivar logs “siempre”: úsalos bufferizados; necesitas trazabilidad.
  • ❌ Copiar sin medir: ab/wrk/vegeta + métricas (CPU, latencia p50/p95/p99, conexiones activas).

Probar y desplegar

nginx -t                    # valida sintaxis
sudo systemctl reload nginx # reload sin cortar conexiones
# si falla, rollback al conf previo
Lenguaje del código: PHP (php)

Herramientas: wrk / vegeta (carga), ss -s (sockets), netstat -anp | grep nginx, ngxtop, Prometheus + exporter.


Conclusión

El Gist de denji es una buena base de laboratorio, pero en producción manda la observación: empieza con valores sanos, mide y ajusta. Si me dices tu caso (estático pesado/API/TLS+HTTP2/3, detrás de CDN o no, tráfico pico/medio), te dejo los valores iniciales más ajustados a tu escenario y una plantilla específica.

vía: GitHub

Suscríbete al boletín SysAdmin

Este es tu recurso para las últimas noticias y consejos sobre administración de sistemas, Linux, Windows, cloud computing, seguridad de la nube, etc. Lo enviamos 2 días a la semana.

¡Apúntate a nuestro newsletter!


– patrocinadores –

Noticias destacadas

– patrocinadores –

¡SUSCRÍBETE AL BOLETÍN
DE LOS SYSADMINS!

Scroll al inicio
×