Nginx puede repartir las peticiones entre varios servidores backend agrupados en un bloque upstream. El algoritmo que elijas decide a qué servidor va cada petición, y eso afecta a la carga de cada máquina, a las sesiones de usuario y a las cachés locales. En esta guía montarás un pequeño laboratorio en Ubuntu 24.04 con tres backends de prueba, verás en la práctica cómo reparte cada algoritmo de Nginx de código abierto y terminarás con una configuración lista para producción con pesos, respaldo, detección de fallos y conexiones keepalive.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • El puerto 80 libre (sin Apache u otro servidor web escuchando).

En producción los backends estarán en otras máquinas, normalmente en una red privada. En el laboratorio los tres backends son sitios de Nginx en 127.0.0.1, lo que permite ver el reparto sin montar más servidores. Las directivas son las mismas.

Resumen de algoritmos

AlgoritmoDirectivaCómo elige servidorÚsalo cuando
Round robin(por defecto)Por turnos, respetando los pesosBackends iguales y peticiones similares
Menos conexionesleast_connEl que tiene menos conexiones activas (ponderado)Peticiones de duración muy variable, WebSocket, descargas largas
Hash de IPip_hashHash de la IP del cliente (los tres primeros octetos en IPv4)Afinidad de sesión simple sin cookies
Hash genéricohash clave [consistent]Hash de cualquier variable (URI, cabecera, argumento)Cachés por URL, afinidad por usuario o por tenant
Aleatoriorandom [two [least_conn]]Al azar, o el mejor de dos al azarVarios balanceadores compartiendo los mismos backends

least_time y las comprobaciones de salud activas (health_check) solo existen en NGINX Plus, la versión comercial. Esta guía se centra en lo que incluye el paquete de Ubuntu.

Paso 1: Instalar Nginx

sudo apt update
sudo apt install nginx

Desactiva el sitio por defecto, que también escucha en el puerto 80:

sudo rm /etc/nginx/sites-enabled/default

Si usas UFW, permite HTTP:

sudo ufw allow 'Nginx HTTP'

Paso 2: Crear tres backends de prueba

Cada backend responde con su nombre, lo que permite ver a qué servidor ha ido cada petición. Crea el archivo:

sudo nano /etc/nginx/conf.d/test-backends.conf
server {
    listen 127.0.0.1:8081;
    default_type text/plain;
    location / { return 200 "backend-1\n"; }
}

server {
    listen 127.0.0.1:8082;
    default_type text/plain;
    location / { return 200 "backend-2\n"; }
}

server {
    listen 127.0.0.1:8083;
    default_type text/plain;
    location / { return 200 "backend-3\n"; }
}

Comprueba la sintaxis, recarga y prueba uno de ellos:

sudo nginx -t
sudo systemctl reload nginx
curl http://127.0.0.1:8082
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
backend-2

Paso 3: Configurar el balanceador con round robin

Crea el sitio del balanceador:

sudo nano /etc/nginx/sites-available/loadbalancer
upstream app_backend {
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://app_backend;
        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_set_header X-Forwarded-Proto $scheme;
    }
}

Sin ninguna directiva de algoritmo, Nginx usa round robin: cada petición va al siguiente servidor de la lista. Activa el sitio y recarga:

sudo ln -s /etc/nginx/sites-available/loadbalancer /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Lanza seis peticiones:

for i in $(seq 1 6); do curl -s http://localhost/; done
backend-1
backend-2
backend-3
backend-1
backend-2
backend-3

Para el resto de la guía solo cambiarás el bloque upstream de /etc/nginx/sites-available/loadbalancer. Después de cada cambio, ejecuta sudo nginx -t && sudo systemctl reload nginx. La recarga no corta las conexiones en curso.

Paso 4: Repartir por capacidad con pesos

Si un servidor tiene el triple de CPU que los demás, dale más peso. El valor por defecto es weight=1:

upstream app_backend {
    server 127.0.0.1:8081 weight=3;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}

Recarga y cuenta cómo se reparten 100 peticiones:

for i in $(seq 1 100); do curl -s http://localhost/; done | sort | uniq -c
     60 backend-1
     20 backend-2
     20 backend-3

Nginx usa un round robin ponderado "suave": intercala los servidores (1, 2, 1, 3, 1...) en lugar de enviar tres peticiones seguidas al mismo. Los pesos también se aplican con least_conn, ip_hash, hash y random.

Paso 5: Menos conexiones activas con least_conn

Round robin asume que todas las peticiones cuestan lo mismo. Si unas tardan 20 ms y otras 20 segundos (informes, subidas de archivos, WebSocket), un servidor puede acumular muchas conexiones lentas mientras otro está libre. least_conn envía cada petición al servidor con menos conexiones activas en relación con su peso:

upstream app_backend {
    least_conn;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}

En el laboratorio las respuestas son instantáneas, así que el reparto se parecerá al de round robin (cuando hay empate, Nginx aplica round robin entre los empatados). La diferencia aparece con tráfico real de duración variable. Para aplicaciones con WebSocket, least_conn suele ser la mejor opción por defecto.

Paso 6: Afinidad de sesión con ip_hash y hash

Si la aplicación guarda la sesión en la memoria de cada servidor, el mismo usuario debe volver siempre al mismo backend. ip_hash lo consigue a partir de la IP del cliente:

upstream app_backend {
    ip_hash;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}
for i in $(seq 1 6); do curl -s http://localhost/; done | sort | uniq -c
      6 backend-3

Todas las peticiones desde la misma IP van al mismo servidor. Ten en cuenta dos limitaciones: en IPv4 solo se usan los tres primeros octetos, así que toda una red /24 cae en el mismo backend, y los usuarios detrás de un mismo NAT corporativo también. Si Nginx está detrás de otro proxy o de una CDN, todas las peticiones llegarán desde la IP del proxy salvo que configures el módulo realip.

Para marcar temporalmente un servidor como fuera de servicio sin alterar el reparto del resto de clientes, usa el parámetro down en lugar de borrar la línea:

    server 127.0.0.1:8083 down;

La directiva hash es más flexible: calcula el hash sobre cualquier variable. Con consistent usa hash consistente (ketama), de modo que al añadir o quitar un servidor solo se reasigna una pequeña parte de las claves en lugar de casi todas. Es la opción adecuada para repartir por URL delante de servidores de caché, o por usuario:

upstream app_backend {
    hash $arg_user consistent;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}
for u in ana luis marta ana luis marta; do
  printf '%s -> ' "$u"; curl -s "http://localhost/?user=$u"
done
ana -> backend-3
luis -> backend-2
marta -> backend-2
ana -> backend-3
luis -> backend-2
marta -> backend-2

Cada usuario siempre llega al mismo backend. Otras claves habituales son $request_uri (cachés), $cookie_sessionid o $http_x_tenant_id.

Paso 7: Algoritmo aleatorio

random elige un servidor al azar teniendo en cuenta los pesos. Con two, elige dos al azar y se queda con el que tiene menos conexiones:

upstream app_backend {
    random two least_conn;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
    server 127.0.0.1:8083;
}

Su ventaja aparece cuando hay varios balanceadores Nginx delante de los mismos backends: cada uno solo conoce sus propias conexiones, y con least_conn puro todos tenderían a enviar tráfico al mismo servidor a la vez. La elección aleatoria entre dos evita ese efecto manada.

Paso 8: Detección de fallos y servidores de respaldo

Nginx de código abierto detecta los fallos de forma pasiva: cuando una petición a un servidor falla, la reintenta en el siguiente y cuenta el fallo. Tras max_fails fallos dentro de fail_timeout, deja de usar ese servidor durante fail_timeout y después vuelve a probarlo con tráfico real.

Vuelve a round robin y añade un servidor que no existe (nada escucha en el puerto 8084) y un servidor de respaldo:

upstream app_backend {
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8084 max_fails=1 fail_timeout=30s;
    server 127.0.0.1:8083 backup;
}

Para que los fallos se detecten rápido, limita también el tiempo de conexión en el bloque location / del mismo archivo:

        proxy_connect_timeout 2s;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;

proxy_next_upstream define qué se considera fallo y se reintenta en otro servidor. Por defecto solo son error y timeout; aquí se añaden las respuestas 502 y 503 del backend. Reintentar solo es seguro para peticiones idempotentes: Nginx no reintenta un POST que ya se envió al backend salvo que añadas non_idempotent, y no deberías hacerlo.

Recarga y prueba:

for i in $(seq 1 6); do curl -s http://localhost/; done
backend-1
backend-2
backend-1
backend-2
backend-1
backend-2

Ningún cliente ha recibido un error: la petición que tocó al puerto 8084 se reintentó en otro servidor y el servidor caído quedó fuera durante 30 segundos. El log de errores lo registra:

sudo grep 8084 /var/log/nginx/error.log | tail -n 1
2026/09/25 10:41:07 [error] 2412#2412: *31 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8084/", host: "localhost"

El servidor backup (backend-3) solo recibe tráfico cuando todos los servidores principales están no disponibles. backup no se puede combinar con ip_hash, hash ni random.

Cuando termines la prueba, elimina la línea del puerto 8084.

Paso 9: Reutilizar conexiones con keepalive

Por defecto Nginx abre una conexión TCP nueva con el backend en cada petición. Con muchas peticiones por segundo, eso añade latencia y llena la tabla de conexiones con sockets en TIME_WAIT. La directiva keepalive mantiene un conjunto de conexiones inactivas abiertas por cada proceso worker:

upstream app_backend {
    least_conn;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8083 backup;

    keepalive 32;
}

Para que funcione, el bloque location debe usar HTTP/1.1 hacia el backend y no reenviar la cabecera Connection: close. La configuración completa de /etc/nginx/sites-available/loadbalancer queda así:

upstream app_backend {
    least_conn;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8083 backup;

    keepalive 32;
}

server {
    listen 80;
    server_name _;

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

        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_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 2s;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

keepalive 32 es el número máximo de conexiones inactivas que cada worker guarda; no limita las conexiones totales. Si necesitas proteger a un backend de un exceso de conexiones, usa el parámetro max_conns en su línea server.

Si el backend es una aplicación con WebSocket, usa un location específico con proxy_set_header Upgrade $http_upgrade; y proxy_set_header Connection "upgrade"; en lugar de la cabecera Connection vacía.

Paso 10: Ver la actividad del balanceador

El módulo stub_status, incluido en el paquete de Ubuntu, ofrece contadores básicos de conexiones. Publícalo solo en localhost:

sudo nano /etc/nginx/conf.d/status.conf
server {
    listen 127.0.0.1:8080;

    location = /nginx_status {
        stub_status;
    }
}
sudo nginx -t && sudo systemctl reload nginx
curl http://127.0.0.1:8080/nginx_status
Active connections: 1
server accepts handled requests
 214 214 229
Reading: 0 Writing: 1 Waiting: 0

Para saber qué backend atendió cada petición y cuánto tardó, añade las variables $upstream_addr y $upstream_response_time a un formato de log propio en /etc/nginx/nginx.conf, dentro del bloque http:

    log_format upstream '$remote_addr "$request" $status '
                        'upstream=$upstream_addr rt=$upstream_response_time';

Y úsalo en el server del balanceador con access_log /var/log/nginx/lb.log upstream;. Si una petición se reintentó, $upstream_addr muestra todos los servidores probados separados por comas.

Qué algoritmo elegir

  • Aplicación sin estado y backends iguales: round robin, con pesos si las máquinas son distintas.
  • Duración de las peticiones muy variable o WebSocket: least_conn.
  • Sesiones guardadas en memoria del servidor: hash $cookie_<nombre> consistent si puedes identificar al usuario, o ip_hash como solución rápida. A largo plazo es mejor mover las sesiones a Redis o a la base de datos y usar round robin.
  • Varios servidores de caché: hash $request_uri consistent, para que cada URL se cachee en un solo servidor.
  • Varios balanceadores Nginx en paralelo: random two least_conn.

Conclusión

Has visto en funcionamiento los algoritmos de balanceo de Nginx de código abierto, cómo influyen los pesos, cómo se comporta el failover pasivo con max_fails y backup, y cómo reducir la latencia con conexiones keepalive hacia los backends. Como siguientes pasos, sustituye los backends de prueba por las IP privadas de tus servidores, añade HTTPS al balanceador con Certbot y, si necesitas comprobaciones de salud activas sin NGINX Plus, valora HAProxy como balanceador delante de tus aplicaciones.