Nginx como Herramienta de Reducción de Costos

Nginx como Herramienta de Reducción de Costos

Reduce tu factura de infraestructura con Nginx: caché que elimina llamadas al backend, compresión que recorta bandwidth, y configuraciones que sustituyen servicios de pago.

Por Omar Flores

Un filtro de agua de alta calidad instalado en la entrada de un edificio no mejora el agua que ya está dentro — evita que el agua sucia entre. Cada litro que filtra es un litro que los sistemas internos no tienen que procesar. El filtro no reemplaza la plomería interior, pero reduce drásticamente la carga sobre ella.

Nginx en la entrada de tu infraestructura funciona igual. Cada petición que resuelve sin llegar al backend es una petición que no ejecuta código, no consulta la base de datos, no consume RAM del servidor de aplicación, y no genera un costo de cómputo en Azure, AWS o cualquier proveedor que uses.

La mayoría de los equipos configura Nginx para que funcione. Pocos lo configuran para que ahorre dinero. La diferencia está en cuatro decisiones de configuración.

El costo real de cada petición que llega al backend

Antes de hablar de Nginx, hay que entender qué cuesta cada petición que llega a tu aplicación.

En Azure Container Apps, el costo de cómputo se calcula por tiempo de CPU activo. Una petición que tarda 50ms en responder consume 50ms de CPU de tu contenedor. Si esa misma respuesta no cambia en 5 minutos — un listado de productos, la página principal, datos de configuración — procesar esa petición 1,000 veces en 5 minutos es 1,000 × 50ms de CPU innecesario.

En AWS, cada invocación de Lambda tiene costo por duración y por número de llamadas. En un servidor EC2 o un VPS, cada petición al backend consume RAM del proceso de la aplicación y ciclos de CPU que podrían atender otras peticiones.

El ancho de banda de salida tiene costo en prácticamente todos los proveedores. Azure cobra después de los primeros 100GB al mes. AWS cobra desde el primer byte. DigitalOcean incluye un límite mensual, pasado el cual cobra por GB.

Nginx puede interceptar esas peticiones antes de que lleguen al backend y responder desde caché, comprimida, con los headers correctos. Cada una de esas intercepciones tiene un costo: casi cero.

Caché de proxy: el ahorro más directo

La caché de proxy de Nginx almacena respuestas del backend en disco y las sirve directamente en peticiones posteriores, sin tocar la aplicación.

El impacto depende del patrón de acceso. Si el 80% de tu tráfico pide las mismas 20 URLs — una landing page, un listado de productos, un endpoint de configuración — y esas URLs no cambian en minutos, un caché de proxy bien configurado puede reducir la carga real al backend en 70-90%.

http {
    # Zona de caché: 100MB de metadatos en RAM, archivos en disco
    # inactive=10m: eliminar entradas no accedidas en 10 minutos
    # max_size=2g: límite total en disco
    proxy_cache_path /var/cache/nginx
                     levels=1:2
                     keys_zone=app_cache:100m
                     max_size=2g
                     inactive=10m
                     use_temp_path=off;

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

            proxy_cache            app_cache;
            proxy_cache_valid      200 5m;     # Cachear respuestas 200 por 5 minutos
            proxy_cache_valid      404 1m;     # Cachear 404 por 1 minuto — evita hammering
            proxy_cache_valid      any 1m;     # Cualquier otro código: 1 minuto

            # Si el backend falla, servir la respuesta cacheada aunque esté vencida
            proxy_cache_use_stale  error timeout updating http_500 http_502 http_503;

            # Solo un request al backend cuando el caché expira — los demás esperan
            # Evita el "cache stampede": 1000 usuarios golpeando el backend simultáneamente
            proxy_cache_lock       on;
            proxy_cache_lock_timeout 5s;

            # Header para monitorear el estado del caché
            add_header X-Cache-Status $upstream_cache_status;

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

proxy_cache_lock es la directiva más importante desde la perspectiva de costos. Sin ella, cuando el caché expira de una URL popular, todas las peticiones que llegan en ese instante pasan al backend al mismo tiempo — un fenómeno llamado thundering herd. Con proxy_cache_lock, solo la primera petición llega al backend. Las demás esperan el resultado y lo sirven desde caché.

Diferenciar qué se cachea y qué no

No todo debe cachearse. Los endpoints de autenticación, las escrituras, y las respuestas personalizadas por usuario no deben ir al caché. La configuración correcta es granular:

# Variables para controlar el bypass del caché
map $request_method $skip_cache {
    default  0;
    POST     1;     # Nunca cachear escrituras
    PUT      1;
    DELETE   1;
    PATCH    1;
}

map $http_authorization $skip_cache_auth {
    default  0;
    "~.+"    1;     # Si hay header Authorization, no cachear (respuesta personalizada)
}

server {
    location /api/ {
        # Bypass si es escritura o petición autenticada
        proxy_cache_bypass $skip_cache $skip_cache_auth;
        proxy_no_cache     $skip_cache $skip_cache_auth;

        proxy_pass  http://backend;
        proxy_cache app_cache;
        proxy_cache_valid 200 5m;
    }

    # Endpoints públicos y estáticos — caché agresivo
    location /api/products {
        proxy_pass  http://backend;
        proxy_cache app_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_key "$scheme$host$request_uri";
    }

    # Endpoints de auth — nunca cachear
    location /api/auth/ {
        proxy_pass http://backend;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
    }
}

Invalidar el caché cuando los datos cambian

El caché de 5 minutos sirve bien para datos que cambian raramente. Para datos que se actualizan y necesitan invalidación inmediata, Nginx soporta purge en la versión comercial. En la versión open source, la estrategia más simple es incluir un identificador de versión en la clave del caché:

# La clave incluye un header de versión que el backend puede incrementar
proxy_cache_key "$scheme$host$request_uri$http_x_cache_version";

El backend, al actualizar datos, puede incluir un header X-Cache-Version con un timestamp o un hash. Las peticiones posteriores con el header nuevo generan una clave diferente — miss de caché y respuesta fresca.

Compresión: reducir bandwidth es reducir costos

Cada byte que Nginx no envía es un byte que no pagas al proveedor de cloud. La compresión gzip reduce HTML, CSS y JavaScript entre 60% y 80%. Para una API que devuelve JSON, la reducción es similar.

El impacto en costos de bandwidth es directo y calculable:

Sin compresión:
- JSON response promedio: 15KB
- 1 millón de requests/mes = 15GB de salida
- AWS US-East: 15GB × $0.09/GB = $1.35/mes

Con gzip (reducción ~70%):
- JSON comprimido: ~4.5KB
- 1 millón de requests/mes = 4.5GB de salida
- AWS US-East: 4.5GB × $0.09/GB = $0.40/mes

Ahorro mensual: $0.95 — en este volumen
A 100 millones de requests: $95/mes de ahorro en bandwidth
http {
    gzip on;
    gzip_vary on;              # Agrega Vary: Accept-Encoding — correcto para CDNs y proxies
    gzip_proxied any;          # Comprimir también respuestas cacheadas de upstream
    gzip_comp_level 6;         # Nivel 6: punto óptimo entre compresión y CPU
                               # Nivel 9: ~2% mejor compresión, 4x más CPU
    gzip_min_length 860;       # No comprimir respuestas pequeñas — overhead supera beneficio
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types
        text/plain
        text/html
        text/css
        text/javascript
        text/xml
        application/javascript
        application/json
        application/xml
        application/rss+xml
        image/svg+xml
        font/ttf
        font/woff
        font/woff2;
    # Nota: image/jpeg, image/png y image/webp NO van aquí
    # Las imágenes ya están comprimidas — gzip las haría más grandes
}

gzip_min_length 860 evita comprimir respuestas pequeñas. Comprimir un JSON de 200 bytes genera un resultado de 190 bytes — el overhead del header gzip supera el ahorro. El umbral de ~1KB es donde la compresión empieza a valer la pena consistentemente.

Assets pre-comprimidos: cero CPU en runtime

Si tu proceso de build genera archivos .gz (gzip) o .br (Brotli), Nginx puede servirlos directamente sin comprimir en cada petición. Esto elimina el costo de CPU de la compresión en tiempo real:

location ~* \.(js|css|json)$ {
    root /var/www/html;

    # Busca app.js.gz antes de intentar comprimir app.js
    gzip_static on;

    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

Con gzip_static on, cuando el cliente acepta gzip y existe archivo.gz en disco, Nginx lo sirve directamente. Sin CPU de compresión, sin tiempo de procesamiento, solo lectura de disco y envío.

Para generar los archivos pre-comprimidos en el build:

# En el Dockerfile o en el pipeline de CI
find /var/www/html -type f \( -name "*.js" -o -name "*.css" -o -name "*.json" \) \
  -exec gzip -9 -k {} \;

# Para Brotli (mejor ratio que gzip, soportado por todos los navegadores modernos)
find /var/www/html -type f \( -name "*.js" -o -name "*.css" \) \
  -exec brotli --best --keep {} \;

Brotli comprime entre 15-25% mejor que gzip para texto. En un sitio con 2MB de assets JavaScript, la diferencia puede ser 400KB menos de transferencia por usuario nuevo.

Caché de assets estáticos: el bandwidth que nunca debería pagarse

Los assets estáticos — JavaScript, CSS, fuentes, imágenes — no cambian entre deploys. Un usuario que visitó tu sitio ayer y vuelve hoy no debería descargar el mismo app.js dos veces. Con la política de caché correcta en Nginx, no lo hace.

server {
    # Assets con hash en el nombre (genera Vite, Astro, Webpack)
    # El hash cambia cuando el contenido cambia — safe to cache forever
    location ~* \.(js|css|woff2|woff|ttf|otf)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Vary "Accept-Encoding";
        access_log off;      # No logear assets — reduce I/O y costo de Log Analytics
        gzip_static on;
    }

    # Imágenes optimizadas
    location ~* \.(webp|avif|jpg|jpeg|png|svg|ico|gif)$ {
        expires 6M;
        add_header Cache-Control "public, max-age=15552000";
        access_log off;
        gzip_static on;
    }

    # HTML y datos: nunca cachear en el navegador — siempre obtener la versión más reciente
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
    }

    # JSON de APIs públicas: caché corto
    location ~* ^/api/public/ {
        proxy_pass http://backend;
        proxy_cache app_cache;
        proxy_cache_valid 200 2m;
        expires 2m;
        add_header Cache-Control "public, max-age=120";
    }
}

immutable en el header Cache-Control le dice al navegador que no intente revalidar el archivo ni al hacer refresh explícito. Es una optimización importante: sin immutable, incluso con max-age=31536000, algunos navegadores envían una petición de validación condicional (If-None-Match) al hacer refresh. Con immutable, no hay esa petición.

El resultado: un usuario que regresa a tu sitio descarga 0 bytes de assets que ya tiene en caché. Para una SPA de 2MB de JavaScript, eso es 2MB de bandwidth que no pagas por cada visita recurrente.

Rate limiting: proteger el presupuesto de ataques

Un ataque de fuerza bruta o un bot de scraping no solo es un problema de seguridad — es un problema de presupuesto. En Azure Container Apps o AWS Lambda, cada petición cuesta dinero. Un bot que hace 10,000 peticiones por minuto puede multiplicar tu factura en horas.

http {
    # Zona general: 30 peticiones/segundo por IP
    limit_req_zone $binary_remote_addr zone=general:20m rate=30r/s;

    # Zona para login: 5 peticiones/minuto por IP
    limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

    # Zona para APIs públicas: 60 peticiones/minuto por IP
    limit_req_zone $binary_remote_addr zone=public_api:20m rate=60r/m;

    # Log de peticiones limitadas para monitoreo
    limit_req_log_level warn;

    server {
        # Rate limit general — protege todo el servidor
        limit_req zone=general burst=50 nodelay;

        location /api/auth/ {
            limit_req zone=auth burst=3 nodelay;
            # Devolver 429 Too Many Requests en lugar del default 503
            limit_req_status 429;
            proxy_pass http://backend;
        }

        location /api/public/ {
            limit_req zone=public_api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

El parámetro $binary_remote_addr usa la representación binaria de la IP — 4 bytes para IPv4, 16 para IPv6 — en lugar del string. Esto reduce el uso de memoria de la zona de tracking a la mitad.

burst define cuántas peticiones adicionales puede acumular un cliente antes de ser rechazado. Con rate=30r/s y burst=50, un cliente puede hacer 80 peticiones instantáneas, pero después está limitado a 30 por segundo. nodelay procesa el burst inmediatamente en lugar de distribuirlo en el tiempo.

Bloquear bots conocidos y rangos de IP maliciosos

Una lista de bots conocidos y rangos de cloud usados para ataques puede bloquear 30-40% del tráfico malicioso antes de que llegue al rate limiter:

# /etc/nginx/conf.d/block-bots.conf
map $http_user_agent $blocked_agent {
    default                       0;
    "~*scrapy"                    1;
    "~*curl"                      1;    # Si tu API no espera llamadas directas de curl
    "~*python-requests"           1;
    "~*Go-http-client"            1;    # Ajustar si tu propia infraestructura usa Go
    "~*masscan"                   1;
    "~*zgrab"                     1;
}

server {
    if ($blocked_agent) {
        return 444;    # 444: Nginx cierra la conexión sin enviar respuesta
                       # El atacante no sabe si el servidor existe
    }
}

return 444 cierra la conexión TCP sin enviar ninguna respuesta HTTP. Desde la perspectiva del atacante, el servidor no existe. Desde la perspectiva del presupuesto, no se ejecutó ningún código en el backend y no se transfirieron datos.

Sustituir servicios de pago con configuración de Nginx

Varios servicios de pago en Azure y AWS hacen lo que Nginx puede hacer gratis con la configuración correcta.

Azure API Management → Nginx como API Gateway

Azure API Management tiene un costo de $50-$400/mes dependiendo del tier. Para equipos pequeños con una sola API, Nginx puede reemplazar las funciones más comunes:

# Nginx como API Gateway — sin Azure API Management

# Rate limiting por API key en lugar de por IP
map $http_x_api_key $api_key_limit {
    default         "anon";    # Sin API key: zona anónima
    "key-cliente-1" "premium"; # Clientes premium: más permisivo
    "key-cliente-2" "standard";
}

limit_req_zone $api_key_limit zone=anon:10m    rate=10r/m;
limit_req_zone $api_key_limit zone=premium:10m rate=1000r/m;
limit_req_zone $api_key_limit zone=standard:10m rate=100r/m;

server {
    # Routing por versión de API
    location /v1/ {
        proxy_pass http://api-v1-backend/;
        add_header X-API-Version "1" always;
    }

    location /v2/ {
        proxy_pass http://api-v2-backend/;
        add_header X-API-Version "2" always;
    }

    # Transformar headers — sin código en la aplicación
    location /api/ {
        proxy_pass http://backend;

        # Añadir headers de correlación
        proxy_set_header X-Request-ID  $request_id;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Eliminar headers internos antes de enviar al cliente
        proxy_hide_header X-Internal-Server;
        proxy_hide_header X-Powered-By;
    }
}

Para equipos con una sola API y sin necesidad de portal de desarrolladores ni análisis avanzado, Nginx cubre el 80% de lo que hace API Management al 0% del costo.

CDN de assets → Nginx con caché larga vida

Un CDN como Azure Front Door cuesta $0.0087 por GB de transferencia más cargos por petición. Para frontends estáticos con tráfico moderado, Nginx con caché de assets bien configurado puede eliminar la necesidad de un CDN:

# Nginx como CDN de primer nivel con caché agresivo
proxy_cache_path /var/cache/nginx/static
                 levels=1:2
                 keys_zone=static_cache:50m
                 max_size=5g
                 inactive=30d;   # Assets no accedidos en 30 días se eliminan

server {
    location ~* \.(js|css|woff2|webp|avif|png|jpg)$ {
        proxy_pass         http://origin_server;
        proxy_cache        static_cache;
        proxy_cache_valid  200 30d;   # Cachear assets 30 días
        proxy_cache_lock   on;

        # Ignorar headers de caché del origen que podrían ser restrictivos
        proxy_ignore_headers Cache-Control Expires;

        expires 30d;
        add_header Cache-Control "public, max-age=2592000, immutable";
        add_header X-Cache-Status $upstream_cache_status always;
        access_log off;
    }
}

Con esta configuración, el primer usuario que visita el sitio descarga los assets del origen. Todos los usuarios posteriores los reciben desde la caché de Nginx en disco. Para un sitio con 2MB de assets y 10,000 visitas al día, el origen solo recibe tráfico cuando los assets cambian.

Azure Front Door para redirecciones → Nginx

Las reglas de reescritura y redirección en Azure Front Door tienen costo por regla procesada. Las mismas reglas en Nginx no cuestan nada adicional:

# Redirecciones sin costo adicional
server {
    listen 80;
    server_name mi-dominio.com;

    # HTTP → HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name www.mi-dominio.com;

    # www → sin www
    return 301 https://mi-dominio.com$request_uri;
}

server {
    listen 443 ssl;
    server_name mi-dominio.com;

    # Reescritura de URLs antiguas → nuevas sin romper SEO
    rewrite ^/blog/post/([0-9]+)$ /articles/$1 permanent;
    rewrite ^/products/(.+)$       /shop/$1     permanent;

    # Ruta antigua de API → nueva versión
    location /api/v1/users {
        rewrite ^ /api/v2/users? last;
    }
}

Logging selectivo: reducir costos de observabilidad

Cada línea de log que Nginx escribe tiene un costo: I/O de disco, y si usas Azure Monitor o AWS CloudWatch, costo de ingesta.

El 95% de los accesos a assets estáticos en el log de Nginx no aportan información útil. Son ruido que pagas por almacenar.

# Formato de log mínimo para producción — solo lo que importa
log_format minimal '$remote_addr "$request" $status $body_bytes_sent '
                   '$request_time "$http_referer"';

# Formato JSON para integración con observabilidad
log_format json_log escape=json
    '{"time":"$time_iso8601",'
    '"ip":"$remote_addr",'
    '"method":"$request_method",'
    '"uri":"$request_uri",'
    '"status":$status,'
    '"bytes":$body_bytes_sent,'
    '"duration":$request_time,'
    '"cache":"$upstream_cache_status"}';

server {
    # Log general con formato mínimo
    access_log /var/log/nginx/access.log minimal;

    # Assets estáticos: sin log — son 80% del tráfico y aportan 0% de información
    location ~* \.(js|css|woff2|webp|png|jpg|ico|svg)$ {
        access_log off;
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # Health checks: sin log — generan ruido constante
    location /health {
        access_log off;
        return 200 "ok\n";
        add_header Content-Type text/plain;
    }

    # Solo la API necesita log completo
    location /api/ {
        access_log /var/log/nginx/api.log json_log;
        proxy_pass http://backend;
    }
}

access_log off en assets estáticos y health checks puede reducir el volumen de logs entre 70-85% dependiendo del tráfico. En Azure Monitor con ingesta a $2.76/GB, eso se traduce directamente en ahorro mensual.

Medir el impacto: antes y después

Sin métricas, la optimización es fe ciega. Nginx expone un endpoint de estadísticas que permite medir el impacto de cada cambio:

# Activar el módulo stub_status
location /nginx_status {
    stub_status on;
    access_log off;
    # Solo accesible desde localhost — no exponer al público
    allow 127.0.0.1;
    deny all;
}
# Ver estadísticas en tiempo real
curl http://localhost/nginx_status

# Output:
# Active connections: 45
# server accepts handled requests
#  1234567 1234567 9876543
# Reading: 0 Writing: 8 Waiting: 37

# Calcular el hit rate del caché
grep "X-Cache-Status" /var/log/nginx/api.log | \
  awk '{print $NF}' | sort | uniq -c | sort -rn

# Output esperado con caché bien configurado:
#  8743 HIT
#  1257 MISS
#  432  BYPASS
# Hit rate: 87.4% — el 87% del tráfico no llegó al backend

Un hit rate de 80%+ significa que el backend recibe el 20% del tráfico real. El costo de cómputo del backend se divide por 5. El ancho de banda de respuestas desde el backend también. La latencia percibida por el usuario mejora porque Nginx responde en microsegundos desde memoria o disco local, no en milisegundos desde la aplicación y la base de datos.

Los números que justifican la configuración

Para que esto no sea abstracto: los números de una API pública con 5 millones de peticiones al mes, 40% de las cuales son al mismo listado de productos que cambia cada 10 minutos.

Sin caché de proxy (5M requests/mes al backend):
- Azure Container Apps (0.5 vCPU): $0.000024/s × 50ms × 5,000,000 = $6/mes en cómputo
- Bandwidth de salida (avg 8KB/response): 5M × 8KB = 40GB → $3.20/mes
- Log Analytics (5M log entries ~2GB): $5.52/mes
Total backend + infra: ~$14.72/mes

Con caché + compresión + logging selectivo:
- 40% de requests servidas desde caché de Nginx: 2M requests no llegan al backend
- Backend recibe 3M requests: cómputo = $3.60/mes
- Compresión reduce bandwidth 70%: 40GB → 12GB = $0.96/mes
- Logging sin assets (70% menos): 0.6GB log → $1.66/mes
Total: ~$6.22/mes

Ahorro mensual: $8.50 — con tráfico moderado
A 50M requests/mes: $85/mes de ahorro continuo

El costo de implementar estas optimizaciones es medio día de trabajo. El ahorro es perpetuo y escala con el tráfico.

Nginx no es solo un servidor web. Es la capa que decide qué trabajo real tiene que hacer tu infraestructura. Configurarlo bien es la decisión de infraestructura con mejor ratio esfuerzo-ahorro disponible.