Cambiar tema

Guía completa del proxy inverso en Nginx: upstream, buffering y timeouts

Easton editorial illustration: one large reverse-proxy gate distributing traffic to three upstream servers

El móvil vibra sin parar: alerta en producción.

Abro los logs y solo veo 502 Bad Gateway. El backend no está caído, pero el timeout en la configuración de Nginx es demasiado corto; en pico de tráfico, las peticiones se cortan antes de terminar. Esa línea proxy_read_timeout 60s la escribí a ojo.

Después del incidente, dediqué una semana a dominar los tres módulos clave del proxy inverso en Nginx: balanceo de carga upstream, buffering con proxy buffer y configuración de timeouts. Si los configuras bien, el proxy aguanta diez veces más tráfico; si los configuras mal, acabas como en aquella alerta.

Este artículo recoge los errores que cometí, lo que aprendí depurando y los principios que por fin entendí. Si trabajas en backend, DevOps o simplemente quieres entender la lógica detrás de los parámetros de Nginx, debería ahorrarte bastante tiempo.


Upstream y balanceo de carga: más que «repartir peticiones»

Primero, la sintaxis básica

El bloque upstream es el núcleo del balanceo de carga en Nginx. Seguro que has visto la forma más simple:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

Parece sencillo: defines un grupo de servidores backend y apuntas proxy_pass ahí. Pero en la práctica no basta. En producción hay mucho más: ¿qué pasa si un servidor cae? ¿Puede una máquina más potente recibir más carga? ¿Mantener conexiones largas?

Cuatro algoritmos de balanceo, cada uno con su escenario

Nginx usa round-robin por defecto: reparte en orden, uno tras otro. Es justo, pero no es inteligente.

Si el backend usa conexiones largas — WebSocket, pool de conexiones a base de datos — el round-robin puede hacer que algunos servidores acumulen conexiones de golpe. Ahí encaja mejor least_conn (menor número de conexiones):

upstream backend {
    least_conn;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

Nginx rastrea las conexiones activas de cada servidor y envía la petición nueva al que esté más libre. En un proyecto con WebSocket para mensajes en tiempo real, con round-robin una máquina se quedó sin memoria; al cambiar a least_conn, la carga se repartió mucho mejor.

Otro caso: el usuario inicia sesión y las peticiones siguientes deben ir al mismo servidor (session en local). IP Hash sirve para eso:

upstream backend {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

Las peticiones del mismo IP van siempre al mismo backend. Pero tiene un fallo: si ese servidor cae, pierdes la sesión. Lo fiable es guardar la session en Redis y usar ip_hash solo como parche temporal.

El cuarto es hash consistente, habitual en caché distribuida:

upstream backend {
    hash $request_uri consistent;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

Nginx crea 160 nodos virtuales por unidad de peso y enruta según el hash del URI. Ventaja: mayor tasa de aciertos — la misma URI siempre va al mismo servidor.

Pesos: cuando las máquinas no son iguales

Es habitual que los backends no tengan la misma potencia: unos con 32 GB RAM y 8 núcleos, otros con 16 GB y 4. ¿Round-robin justo? Desperdicias la máquina más fuerte.

upstream backend {
    server 192.168.1.10:8080 weight=3;
    server 192.168.1.11:8080 weight=2;
    server 192.168.1.12:8080 weight=1;
}

weight=3 recibe el triple de peticiones. Más potencia, más trabajo — tiene sentido.

También está backup, el servidor de reserva:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080 backup;
}

El backup no entra en el balanceo hasta que los dos principales fallen. Como un suplente: entra cuando los titulares no pueden jugar.

Pool keepalive: el secreto para duplicar rendimiento

Mucha gente lo pasa por alto. Por defecto Nginx abre una conexión TCP nueva al backend en cada petición y la cierra al terminar. Suena bien, pero no lo es.

Establecer TCP cuesta tres paquetes; cerrar, cuatro. En alta concurrencia, el costo es enorme. El pool keepalive reutiliza conexiones y elimina ese overhead.

Ejemplo de configuración:

upstream backend {
    server 192.168.1.10:8080;
    keepalive 32;  # Cada worker mantiene 32 conexiones inactivas
}

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

Dos detalles:

  1. keepalive 32 fija el máximo de conexiones inactivas por proceso worker
  2. Hay que poner proxy_http_version 1.1 y Connection "" — HTTP/1.0 no soporta conexiones persistentes

Probé un API sin keepalive: unos 2000 QPS; con keepalive, más de 4000. Duplicar no es exageración, es real.

2x
Mejora de QPS
Source: Datos medidos: tras activar el pool keepalive

Cuidado: no subas keepalive demasiado. En pruebas puse 100 y el backend tenía un solo contenedor ECS — lo saturé de conexiones. En producción, la fórmula aproximada es:

keepalive ≈ QPS total ÷ tiempo medio por petición ÷ número de procesos worker

Si estimas QPS 10000, tiempo medio 50 ms y 4 workers:

10000 × 0.05 ÷ 4 = 125

keepalive ≈ 125 suele ser razonable.

Comprobación de salud: expulsar automáticamente los caídos

La versión open source de Nginx solo tiene comprobación pasiva: marca el servidor como no saludable cuando falla una petición:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
    }
}

proxy_next_upstream define cuándo reintentar en otro servidor: error de conexión, timeout o respuestas 502/503/504. proxy_next_upstream_tries 3 limita a 3 intentos.

La comprobación pasiva tiene retraso: solo descubres el fallo cuando una petición falla. Si la disponibilidad es crítica, la comprobación activa de NGINX Plus es mejor:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

server {
    location / {
        proxy_pass http://backend;
        health_check interval=5s fails=3 passes=2;
    }
}

Cada 5 segundos envía una petición de salud; tras 3 fallos consecutivos marca unhealthy; tras 2 éxitos seguidos recupera el estado.


Proxy buffer: ¿aliado o estorbo?

Para qué sirve el buffering

Concepto clave: cuando Nginx recibe la respuesta del backend, no la envía al cliente de inmediato — la guarda en un buffer.

¿Por qué? La velocidad del cliente es impredecible. El backend puede soltar datos rápido, pero si el cliente es lento, Nginx tiene que esperar. Con buffers, Nginx puede almacenar la respuesta entera y enviarla poco a poco — el backend no espera y puede atender la siguiente petición.

El costo: memoria. Con cuerpos grandes y mucha concurrencia, el consumo sube.

Tres parámetros clave y cómo se relacionan

proxy_buffer_size 4k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;

Al principio me liaba — nombres parecidos, significados enredados. Un esquema lo aclaró:

  • proxy_buffer_size: buffer para cabeceras de respuesta, uno por petición
  • proxy_buffers: array de buffers para el cuerpo; formato cantidad tamaño_cada_uno
  • proxy_busy_buffers_size: parte del buffer que se está enviando al cliente; no puede superar la mitad del total de buffers

Ejemplo: proxy_buffers 8 32k → 8×32k = 256k total. proxy_busy_buffers_size 64k es un cuarto, dentro de la regla.

¿Cuándo ajustarlos?

Si las cabeceras del backend son muy grandes (muchas cookies), puede aparecer «upstream sent too big header». Solución: subir proxy_buffer_size:

proxy_buffer_size 16k;

Si el cuerpo suele ser grande (JSON enorme), amplía los buffers:

proxy_buffers 16 64k;

Escenarios especiales: hay que desactivar el buffering

A veces el buffering estorba.

Server-Sent Events (SSE): el backend empuja un flujo continuo; si Nginx retiene, el cliente recibe con retraso. Desactiva el buffering:

location /events {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 86400s;
}

proxy_read_timeout 86400s (un día) porque SSE es conexión larga; no puede cortarse por timeout.

WebSocket: comunicación bidireccional en tiempo real, igual:

location /ws {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400s;
}

Subida de archivos grandes: si el cliente sube 1 GB y Nginx lo recibe entero antes de reenviar, la memoria revienta. Desactiva el buffering de la petición:

location /upload {
    proxy_pass http://backend;
    proxy_request_buffering off;
    client_max_body_size 1G;
}

proxy_request_buffering off hace streaming: recibe y reenvía al mismo tiempo.


Timeouts: la lógica detrás de la configuración

Tres parámetros, tres responsabilidades

proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;

Los nombres se parecen, pero cada uno hace lo suyo:

  • proxy_connect_timeout: tiempo máximo para establecer la conexión TCP con el backend. Si el servidor tarda (congestión, firewall), se abandona al superarlo.
  • proxy_read_timeout: tras conectar, tiempo máximo entre dos lecturas de datos del backend.
  • proxy_send_timeout: límite de tiempo para enviar el cuerpo de la petición al backend.

Punto que confunde: proxy_read_timeout no es el tiempo total de la respuesta, sino el intervalo entre dos lecturas. Si el backend tarda 5 minutos pero envía datos de vez en cuando (keepalive), proxy_read_timeout 60s puede bastar. Si permanece en silencio 5 minutos seguidos, necesitas proxy_read_timeout 300s.

Relación entre timeouts y 502/504

La lección de aquella alerta a las tres de la madrugada:

  • 502 Bad Gateway: Nginx no llegó a conectar con el backend — servicio caído, puerto cerrado, firewall
  • 504 Gateway Timeout: sí conectó, pero el backend tardó demasiado en devolver datos

Ejemplo: con proxy_connect_timeout 10s, si el backend tarda 15 s en aceptar la conexión, obtienes 502. Si la conexión es rápida pero el procesamiento dura 2 minutos y proxy_read_timeout 60s, obtienes 504.

Estrategias de timeout según el escenario

Servicios API: 30-60 segundos suelen bastar. Las APIs deben responder rápido; un timeout corto detecta peticiones lentas:

proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;

Procesamiento de archivos: exportar informes, generar PDF — pueden tardar minutos. Relaja los timeouts:

proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

Servicios en streaming: vídeo en directo, WebSocket, SSE — conexiones largas; un día también es válido:

proxy_read_timeout 86400s;

Diagnóstico práctico de 502/504

Análisis de causas

Casos que he visto:

  1. El upstream realmente caído: proceso muerto, puerto ocupado, desbordamiento de memoria
  2. Conexiones agotadas: el pool del backend está lleno y Nginx no puede conectar
  3. Timeouts demasiado cortos: como mi alerta nocturna — proxy_read_timeout 60s con un backend que tarda 2 minutos
  4. Firewall o red: reglas de seguridad mal configuradas, iptables bloqueando

Diagnóstico por logs

El primer paso siempre es el error_log:

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

Mensajes habituales:

upstream timed out (110: Connection timed out) while reading response header from upstream

Eso es 504, timeout de lectura.

connect() failed (111: Connection refused) while connecting to upstream

Eso es 502, conexión rechazada — el backend no escucha.

Un nivel más: formato de log personalizado con estado del upstream:

log_format upstream_status '$status $upstream_status $upstream_response_time';

access_log /var/log/nginx/access.log upstream_status;

Verás algo como 200 200, 200, 502 0.5, 1.2, 3.0 — qué backend devolvió qué código y cuánto tardó.

Soluciones típicas

Escenario 1: backend lento, 504 frecuentes

Solución: sube proxy_read_timeout y confirma que el backend puede terminar a tiempo. No ajustes solo Nginx; sincroniza también los timeouts del backend.

Escenario 2: conexión rechazada, 502

Solución: comprueba que el proceso corre, que el puerto escucha y que el firewall permite el tráfico.

netstat -tlnp | grep 8080
ps aux | grep your_app

Escenario 3: conexiones agotadas bajo alta concurrencia

Solución: aumenta el límite del pool del backend o activa keepalive en upstream de Nginx para reducir conexiones nuevas.


Buenas prácticas de optimización

Configuración de workers

Nginx es multiproceso. worker_processes fija el número de procesos; suele igualar los núcleos de CPU:

worker_processes auto;

auto detecta los núcleos. En una máquina de 8 núcleos tendrás 8 workers.

worker_connections es el máximo de conexiones por worker:

events {
    worker_connections 4096;
}

Conexiones concurrentes teóricas = worker_processes × worker_connections. 8 × 4096 = 32768. En la práctica también limita el límite de descriptores de archivo del sistema.

Las tres palancas TCP

sendfile on;
tcp_nopush on;
tcp_nodelay on;

Combinadas mejoran el rendimiento de forma notable:

  • sendfile on: transmisión a nivel de kernel, sin pasar por el espacio de usuario
  • tcp_nopush on: con sendfile, agrupa paquetes en lugar de enviarlos uno a uno
  • tcp_nodelay on: paquetes pequeños se envían al instante, sin esperar a llenar el buffer

En un servicio de archivos estáticos, con estos tres activos el throughput subió más de un 30 %.

30%+
Mejora de throughput
Source: Datos medidos: sendfile + tcp_nopush + tcp_nodelay activados

Otras optimizaciones

Compresión gzip: comprime respuestas de texto y ahorra ancho de banda:

gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;

Límite de descriptores de archivo: en alta concurrencia puede faltar. Revisa el límite del sistema:

ulimit -n

Si solo hay 1024, súbelo. Edita /etc/security/limits.conf:

* soft nofile 65535
* hard nofile 65535

Ejemplo de configuración completa

Plantilla recomendada para producción:

# Configuración básica
worker_processes auto;

events {
    worker_connections 4096;
    multi_accept on;
}

http {
    # Optimización TCP
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Keepalive
    keepalive_timeout 30;
    keepalive_requests 100;

    # Configuración de buffering
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;

    # Configuración de timeouts
    proxy_connect_timeout 10s;
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;

    # gzip
    gzip on;
    gzip_types text/plain text/css application/json;

    upstream backend {
        least_conn;
        server 192.168.1.10:8080 weight=3;
        server 192.168.1.11:8080 weight=2;
        server 192.168.1.12:8080 backup;
        keepalive 32;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;

            proxy_next_upstream error timeout http_502 http_503 http_504;
            proxy_next_upstream_tries 3;
        }

        # Configuración dedicada para SSE
        location /events {
            proxy_pass http://backend;
            proxy_buffering off;
            proxy_read_timeout 86400s;
        }
    }
}

Resumen

En el fondo, son tres pilares:

  1. Configuración upstream: elige el algoritmo de balanceo, activa el pool keepalive y configura comprobaciones de salud
  2. Configuración de buffering: entiende la relación entre los tres parámetros; en escenarios especiales desactiva el buffering
  3. Configuración de timeouts: sabe qué controla cada parámetro y adapta la estrategia al escenario

La alerta de las tres de la madrugada me enseñó algo: configurar Nginx no es rellenar parámetros. Cada uno tiene una lógica de diseño; entenderla evita caer en las mismas trampas.

Si acabas de empezar con Nginx, parte de la configuración por defecto y ajusta cuando surja un problema — no hagas como yo, poniendo proxy_read_timeout 60s a ciegas en producción. Si ya has pisado algún error, este artículo debería ayudarte a unir experiencias sueltas en un sistema.

Al terminar de escribir, revisé mi configuración actual en producción: keepalive 32, proxy_read_timeout 120s, least_conn. Desde entonces, ninguna alerta a las tres de la madrugada.


FAQ

¿proxy_read_timeout es un timeout total o el intervalo entre dos lecturas?
Es el intervalo entre dos operaciones de lectura. Si el backend envía datos de forma continua durante el procesamiento (por ejemplo, paquetes de keepalive), aunque el procesamiento total dure 5 minutos, proxy_read_timeout 60s puede bastar. Pero si el backend permanece en silencio 5 minutos completos, hay que subirlo a 300s.
¿Cuándo conviene desactivar proxy_buffering?
Hay tres escenarios obligatorios:

• Server-Sent Events (SSE): push en tiempo real; el buffering retrasa los mensajes
• WebSocket: comunicación bidireccional en tiempo real; hace falta transmisión en streaming
• Subida de archivos grandes: evita reventar la memoria; recibir y reenviar al mismo tiempo
¿Qué valor conviene para keepalive?
Fórmula: keepalive ≈ QPS total × tiempo medio por petición ÷ número de procesos worker. Por ejemplo, QPS 10000, tiempo de respuesta 50 ms, 4 workers: keepalive ≈ 125. No lo subas demasiado — yo llegué a poner 100 y saturé el backend.
¿Cuál es la diferencia entre 502 y 504?
502 Bad Gateway indica que Nginx no puede conectar con el backend (servicio caído, puerto cerrado, firewall bloqueando). 504 Gateway Timeout indica que sí conectó pero la respuesta expiró (backend lento). El diagnóstico es distinto: en 502 revisa procesos y puertos; en 504 revisa timeouts y tiempo de procesamiento del backend.
¿Qué algoritmo de balanceo de carga elegir?
Según el escenario:

• Round-robin (por defecto): servicios sin estado, reparto equitativo
• least_conn: conexiones largas (WebSocket, pool de conexiones a base de datos)
• ip_hash: afinidad de sesión (solución temporal; Redis es más fiable)
• hash: caché distribuida, mayor tasa de aciertos
¿Cómo resolver upstream sent too big header?
Aumenta proxy_buffer_size. Si las cabeceras de respuesta del backend son muy grandes (por ejemplo, muchas cookies), superan el buffer por defecto de 4k. Cambiar a proxy_buffer_size 16k suele resolverlo.
¿Por qué sendfile + tcp_nopush + tcp_nodelay mejoran el rendimiento?
sendfile evita el espacio de usuario y transmite directamente desde el kernel; tcp_nopush agrupa envíos y reduce paquetes; tcp_nodelay envía datos pequeños al instante sin esperar. Combinados, el throughput de archivos estáticos sube más de un 30 % en pruebas reales.

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

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog