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

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:
keepalive 32fija el máximo de conexiones inactivas por proceso worker- Hay que poner
proxy_http_version 1.1yConnection ""— 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.
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ónproxy_buffers: array de buffers para el cuerpo; formatocantidad tamaño_cada_unoproxy_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:
- El upstream realmente caído: proceso muerto, puerto ocupado, desbordamiento de memoria
- Conexiones agotadas: el pool del backend está lleno y Nginx no puede conectar
- Timeouts demasiado cortos: como mi alerta nocturna —
proxy_read_timeout 60scon un backend que tarda 2 minutos - 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 usuariotcp_nopush on: con sendfile, agrupa paquetes en lugar de enviarlos uno a unotcp_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 %.
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:
- Configuración upstream: elige el algoritmo de balanceo, activa el pool keepalive y configura comprobaciones de salud
- Configuración de buffering: entiende la relación entre los tres parámetros; en escenarios especiales desactiva el buffering
- 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?
¿Cuándo conviene desactivar proxy_buffering?
• 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?
¿Cuál es la diferencia entre 502 y 504?
¿Qué algoritmo de balanceo de carga elegir?
• 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?
¿Por qué sendfile + tcp_nopush + tcp_nodelay mejoran el rendimiento?
11 min de lectura · Publicado el: 30 mar 2026 · Actualizado el: 21 ago 2026
Guía práctica de Nginx
Estás leyendo el primer artículo de esta serie. Continúa con el siguiente o abre el hub para ver toda la ruta.



Comentarios
Inicia sesión con GitHub para dejar un comentario