HAProxy incluye una caché HTTP en memoria que guarda las respuestas de los backends y las sirve directamente, sin herramientas adicionales como Varnish. Está pensada como una caché pequeña y rápida (a veces llamada favicon cache) para recursos que se piden mucho y cambian poco. En este tutorial activarás la caché en HAProxy sobre Ubuntu 24.04, ajustarás el tiempo de vida de las entradas, excluirás las rutas que no deben cachearse y verificarás los aciertos.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Una aplicación HTTP escuchando en 127.0.0.1:8080. Si no tienes una, el paso 2 levanta un backend de prueba.
  • Conocimientos básicos de la estructura de haproxy.cfg (frontend, backend).

Paso 1: Instalar HAProxy

Ubuntu 24.04 incluye HAProxy 2.8, una versión LTS con la caché completa. Instálalo junto con socat, que usarás para consultar el estado de la caché:

sudo apt update
sudo apt install haproxy socat
haproxy -v | head -n 1
HAProxy version 2.8.x 2024/xx/xx - https://haproxy.org/

Paso 2: Levantar un backend de prueba

Si ya tienes tu aplicación en el puerto 8080, salta este paso. Si no, sirve un directorio con Python en otra sesión SSH:

mkdir -p ~/backend-demo/static
echo '<h1>Hola desde el backend</h1>' > ~/backend-demo/index.html
echo 'body { color: #333; }' > ~/backend-demo/static/app.css
python3 -m http.server 8080 --bind 127.0.0.1 --directory ~/backend-demo

Este servidor envía la cabecera Last-Modified, que HAProxy necesita para considerar la respuesta cacheable cuando no hay Cache-Control.

Paso 3: Entender qué cachea HAProxy

Antes de configurar nada conviene saber las reglas, porque son más estrictas que las de Nginx o Varnish. HAProxy solo guarda una respuesta cuando se cumple todo lo siguiente:

CondiciónDetalle
MétodoSolo GET.
Versión HTTPLa petición debe ser HTTP/1.1 o superior.
Código de estadoSolo 200.
AutorizaciónLa petición no puede llevar cabecera Authorization.
CaducidadLa respuesta debe tener Cache-Control: max-age o s-maxage, Expires, o un validador (ETag o Last-Modified).
TamañoCabeceras más cuerpo no deben superar max-object-size.
VarySin process-vary on, las respuestas con Vary no se cachean.

Además, la caché vive en memoria compartida y se pierde al reiniciar HAProxy. Un reload también empieza con la caché vacía.

Paso 4: Definir la caché y activarla

El archivo /etc/haproxy/haproxy.cfg de Ubuntu ya trae las secciones global y defaults, con el socket de administración en /run/haproxy/admin.sock. Haz una copia y ábrelo:

sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig
sudo nano /etc/haproxy/haproxy.cfg

Añade al final del archivo, sin tocar lo que ya hay:

cache app_cache
    total-max-size 256
    max-object-size 1048576
    max-age 300

frontend web
    bind :80
    mode http
    default_backend app

    http-response set-header X-Cache-Status HIT if { res.cache_hit }
    http-response set-header X-Cache-Status MISS if !{ res.cache_hit }

backend app
    mode http
    http-request cache-use app_cache
    http-response cache-store app_cache
    server app1 127.0.0.1:8080 check

Qué hace cada parte:

  • total-max-size 256 reserva 256 MB de RAM para la caché. El valor se expresa en megabytes y el máximo es 4095.
  • max-object-size 1048576 limita cada objeto a 1 MB. Se expresa en bytes y no puede superar la mitad de total-max-size. Los objetos más grandes simplemente no se cachean.
  • max-age 300 es el tiempo máximo en segundos. Si el backend indica un max-age menor, se usa el del backend; si indica uno mayor, se recorta a 300. Si no se configura, el valor por defecto es 60 segundos.
  • cache-use busca la respuesta en la caché antes de enviar la petición al servidor y cache-store guarda la respuesta al volver.
  • La muestra res.cache_hit indica si la respuesta salió de la caché. Se usa en el frontend para añadir la cabecera X-Cache-Status de diagnóstico.

Valida la configuración antes de aplicarla:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Configuration file is valid

Recarga el servicio:

sudo systemctl reload haproxy
sudo systemctl status haproxy --no-pager

Paso 5: Comprobar que la caché funciona

Pide la misma página dos veces:

curl -s -o /dev/null -D - http://127.0.0.1/ | grep -iE 'x-cache-status|^age'
curl -s -o /dev/null -D - http://127.0.0.1/ | grep -iE 'x-cache-status|^age'
X-Cache-Status: MISS
X-Cache-Status: HIT
age: 3

HAProxy añade la cabecera Age a las respuestas servidas desde caché, con los segundos que lleva almacenada la entrada. En la terminal del backend de prueba verás que solo llegó la primera petición.

Consulta el contenido de la caché a través del socket de administración:

echo "show cache" | sudo socat stdio /run/haproxy/admin.sock

La salida muestra, para cada caché, los bloques libres y las entradas almacenadas con su tamaño y el tiempo que les queda de vida.

Paso 6: Controlar el TTL y excluir rutas

En una aplicación real no todo debe cachearse igual. Un patrón habitual es cachear solo los recursos estáticos, con un TTL largo, y dejar pasar el resto. Edita el backend:

sudo nano /etc/haproxy/haproxy.cfg

Sustituye la sección backend app por esta versión:

backend app
    mode http
    acl es_estatico path_beg /static/
    acl es_estatico path_end .css .js .png .jpg .svg .woff2
    acl sesion req.cook(sessionid) -m found

    http-request set-var(txn.cacheable) bool(true) if es_estatico !sesion
    http-request cache-use app_cache if { var(txn.cacheable) -m bool }

    http-response set-header Cache-Control "public, max-age=300" if { var(txn.cacheable) -m bool } !{ res.hdr(Cache-Control) -m found }
    http-response cache-store app_cache if { var(txn.cacheable) -m bool }

    server app1 127.0.0.1:8080 check

Cómo funciona:

  • Las ACL es_estatico se definen dos veces con el mismo nombre; HAProxy las combina con un OR, así que basta con que se cumpla una.
  • La variable txn.cacheable se marca al recibir la petición cuando es un recurso estático y no lleva la cookie de sesión sessionid (sustitúyela por la de tu aplicación). Es necesaria porque las muestras de la petición, como path o req.cook, no están disponibles en las reglas http-response: HAProxy avisaría de que la ACL nunca coincide. Las variables con ámbito txn duran toda la transacción y sí se pueden leer en la respuesta.
  • cache-use y cache-store solo actúan sobre las peticiones marcadas. El resto va siempre al backend y nunca se guarda.
  • Si el backend no envía Cache-Control para un recurso estático, HAProxy le añade max-age=300 antes de guardarlo. Las reglas http-response se evalúan en orden, por eso la línea set-header va antes de cache-store.

Valida y recarga:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy

Comprueba que el CSS se cachea y la página principal no:

curl -s -o /dev/null -D - http://127.0.0.1/static/app.css | grep -i x-cache-status
curl -s -o /dev/null -D - http://127.0.0.1/static/app.css | grep -i x-cache-status
curl -s -o /dev/null -D - http://127.0.0.1/ | grep -i x-cache-status
curl -s -o /dev/null -D - http://127.0.0.1/ | grep -i x-cache-status
X-Cache-Status: MISS
X-Cache-Status: HIT
X-Cache-Status: MISS
X-Cache-Status: MISS

Paso 7 (opcional): Respuestas con cabecera Vary

Si tu backend comprime las respuestas y envía Vary: Accept-Encoding, HAProxy no las cacheará salvo que actives la gestión de variantes. Añade estas líneas a la sección cache app_cache:

    process-vary on
    max-secondary-entries 10

Con process-vary on, HAProxy guarda una variante por cada valor distinto de las cabeceras que sabe gestionar, como Accept-Encoding. Si el backend usa Vary con otras cabeceras (por ejemplo Cookie o User-Agent), esas respuestas siguen sin cachearse, lo cual es el comportamiento correcto. max-secondary-entries limita cuántas variantes se guardan de una misma URL.

Valida y recarga de nuevo con sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy.

Paso 8: Abrir el puerto en el firewall

Si HAProxy va a recibir tráfico desde internet, permite el puerto 80 (y el 443 cuando configures TLS):

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Para HTTPS, concatena certificado y clave privada en un único archivo PEM y añade bind :443 ssl crt /etc/haproxy/certs/your_domain.pem al frontend. La caché funciona igual con tráfico cifrado porque HAProxy termina el TLS.

Solución de problemas

Siempre MISS. Revisa la tabla del paso 3 contra las cabeceras reales del backend con curl -sI http://127.0.0.1:8080/ruta. Las causas más frecuentes son un código distinto de 200, falta de Cache-Control, Expires y validadores, una cabecera Vary o un cuerpo mayor que max-object-size.

haproxy -c rechaza max-object-size. El valor no puede superar la mitad de total-max-size. Reduce uno o aumenta el otro, y recuerda que max-object-size va en bytes y total-max-size en megabytes.

La caché se vacía sola. Cada systemctl reload o restart arranca con la caché vacía. Es normal: la caché de HAProxy es solo en memoria y está pensada para objetos de vida corta.

show cache devuelve Permission denied. Ejecuta socat con sudo, o añade tu usuario al grupo haproxy, dueño del socket.

Conclusión

HAProxy ya cachea en memoria los recursos estáticos de tu aplicación, respeta el TTL que envía el backend con un máximo de 300 segundos y deja pasar las peticiones con sesión. Como siguientes pasos puedes añadir más servidores al backend para balancear carga, activar la página de estadísticas de HAProxy para vigilar el tráfico o colocar Varnish o una CDN delante si necesitas una caché grande en disco con purga selectiva.