HAProxy Caching Configuration

HAProxy introduced native HTTP caching in version 1.8, allowing it to cache GET responses in memory and serve them directly without forwarding requests to backend servers. This guide covers enabling HAProxy's built-in cache, configuring TTL and Vary header handling, optimizing cache size, and monitoring cache hit rates.

Prerequisites

  • Ubuntu 20.04/22.04 or CentOS 8/Rocky Linux 8+
  • HAProxy 2.2 or later (cache improvements in 2.x)
  • Root or sudo access
  • An existing web application or upstream backend servers

Install HAProxy 2.x

Ubuntu/Debian:

# Add the official HAProxy repository for latest 2.x
sudo add-apt-repository ppa:vbernat/haproxy-2.8 -y
sudo apt update
sudo apt install -y haproxy=2.8.*

# Verify version (must be 2.x for good caching support)
haproxy -v
# Expected: HAProxy version 2.8.x

sudo systemctl enable haproxy

CentOS/Rocky Linux:

# Install from the official HAProxy repo
sudo dnf install -y haproxy

# For a newer version, use the SCL or compile from source
haproxy -v

sudo systemctl enable haproxy

Backup the default configuration:

sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak

Enable HTTP Caching

HAProxy's cache is defined in the cache section and referenced in a backend:

# /etc/haproxy/haproxy.cfg

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
    maxconn 50000

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    option  forwardfor
    option  http-server-close
    timeout connect 5s
    timeout client  30s
    timeout server  30s

# --- CACHE DEFINITION ---
cache my_cache
    total-max-size 512   # Maximum cache size in MB
    max-object-size 10   # Maximum single cached object size in MB
    max-age 300          # Default maximum TTL in seconds (5 minutes)
    process-vary on      # Handle Vary headers (HAProxy 2.1+)

Frontend and Backend Configuration

# Frontend - accepts client connections
frontend http_front
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # Redirect HTTP to HTTPS
    http-request redirect scheme https unless { ssl_fc }

    default_backend app_servers

# Backend - upstream application servers
backend app_servers
    balance roundrobin

    # Enable caching for this backend
    http-request cache-use my_cache

    # Store responses in cache
    http-response cache-store my_cache

    # Only cache GET and HEAD requests
    http-request cache-use my_cache if { method GET HEAD }

    server app1 192.168.1.10:8080 check
    server app2 192.168.1.11:8080 check

    # Add cache status header to responses
    http-response set-header X-Cache-Status %[res.cache_hit]

    # Health check
    option httpchk GET /health
    http-check expect status 200

Test the configuration:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxy
sudo systemctl status haproxy

# Test caching
curl -I https://example.com/api/v1/products
# Look for: X-Cache-Status: 1 (hit) or 0 (miss)

TTL Management

Control how long responses are cached:

backend app_servers
    balance roundrobin

    http-request cache-use my_cache
    http-response cache-store my_cache

    # Override TTL for specific paths using ACLs
    acl is_static path_end .css .js .png .jpg .gif .svg .ico .woff2
    acl is_api path_beg /api/v1/
    acl is_private path_beg /api/v1/user /api/v1/cart /api/v1/orders

    # Don't cache private/personal endpoints
    http-response cache-store my_cache unless is_private

    # Set custom max-age via Cache-Control header injection
    # For static assets: long TTL
    http-response set-header Cache-Control "public, max-age=86400" if is_static

    # For API responses: short TTL
    http-response set-header Cache-Control "public, max-age=60" if is_api !is_private

    # For private data: no caching
    http-response set-header Cache-Control "private, no-store" if is_private

    server app1 192.168.1.10:8080 check
    server app2 192.168.1.11:8080 check

The max-age in the cache block sets the absolute upper limit; the upstream Cache-Control: max-age takes effect if lower.

Vary Header Handling

The Vary header indicates that cache entries should vary by request header values:

# /etc/haproxy/haproxy.cfg

cache my_cache
    total-max-size 512
    max-object-size 10
    max-age 300
    process-vary on     # Enable Vary header processing (required)

backend app_servers
    http-request cache-use my_cache
    http-response cache-store my_cache

    # Normalize Accept-Encoding to avoid unnecessary cache variations
    # Compress Accept-Encoding to a canonical value
    http-request set-header Accept-Encoding "gzip" if { hdr(Accept-Encoding) -i -m sub gzip }

    # Cache varies by Accept header (JSON vs XML)
    # HAProxy will create separate cache entries for each Accept value
    # when process-vary is on and upstream sends Vary: Accept

    server app1 192.168.1.10:8080 check

Caveats:

  • process-vary on is required to respect Vary: Accept-Encoding etc.
  • Too many Vary dimensions reduce cache efficiency
  • Normalize headers (Accept-Encoding, Accept-Language) before they reach the cache layer

Cache Size Optimization

cache my_cache
    # total-max-size: total RAM for cache storage (in MB)
    # Recommended: 10-20% of available RAM
    # For a 4GB server: ~400-800MB
    total-max-size 800

    # max-object-size: skip objects larger than this (in MB)
    # Avoids caching large downloads that won't benefit much
    max-object-size 5

    # max-age: upper TTL limit regardless of Cache-Control
    max-age 3600

    # process-vary: required for correct Vary header handling
    process-vary on

Calculate optimal settings:

# Check available RAM
free -h

# Estimate entries:
# 512MB cache / avg 50KB object size = ~10,000 cached objects
# Adjust total-max-size based on your object sizes

# Monitor cache usage via HAProxy stats socket
echo "show cache" | sudo socat stdio /run/haproxy/admin.sock

Monitor Cache Hit Rates

Enable the HAProxy statistics page:

# Add to haproxy.cfg
frontend stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 30s
    stats auth admin:securepassword
    stats show-legends
    stats show-node
    
    # Restrict access
    acl network_allowed src 127.0.0.1 10.0.0.0/8
    stats http-request deny unless network_allowed
# Via stats socket (real-time)
# Show cache stats
echo "show cache" | sudo socat stdio /run/haproxy/admin.sock

# Monitor via HTTP
curl -u admin:securepassword http://localhost:8404/stats

# Parse cache info from socket
echo "show info" | sudo socat stdio /run/haproxy/admin.sock | grep -i cache

# Count hits/misses from logs
# Configure log format to include cache status
# In defaults section:
# log-format "%ci:%cp [%t] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Ta %ST %B %tsc %ac/%fc/%bc/%sc/%rc %{+Q}r %[res.hdr(X-Cache-Status)]"

awk '{print $NF}' /var/log/haproxy.log | sort | uniq -c

Advanced Cache Rules

backend app_servers
    http-request cache-use my_cache
    http-response cache-store my_cache

    # Conditional caching with ACLs
    acl is_get method GET
    acl is_head method HEAD
    acl has_auth hdr(Authorization) -m found
    acl is_cacheable path_beg /api/v1/public /static /media

    # Only cache GET/HEAD on public paths without auth
    http-request cache-use my_cache if is_get is_cacheable !has_auth
    http-response cache-store my_cache if is_get is_cacheable !has_auth

    # Ignore Pragma: no-cache from clients (force cache)
    http-request del-header Pragma

    # Strip Set-Cookie from cacheable responses to allow caching
    # CAUTION: Only do this for truly public, session-independent responses
    # http-response del-header Set-Cookie if is_cacheable

    # Add custom headers to help debug
    http-response set-header X-Served-By %[env(HOSTNAME)]
    http-response set-header X-Cache-Status %[res.cache_hit]

    server app1 192.168.1.10:8080 check inter 10s
    server app2 192.168.1.11:8080 check inter 10s

Troubleshooting

HAProxy version doesn't support cache:

# Check version
haproxy -v
# Cache requires 1.8+, Vary support requires 2.1+

# Install newer version
sudo add-apt-repository ppa:vbernat/haproxy-2.8 -y
sudo apt install haproxy

Responses not being cached (cache always misses):

# Check upstream Cache-Control headers
curl -I http://192.168.1.10:8080/api/v1/products | grep -i cache-control
# If "Cache-Control: private" or "no-store", HAProxy won't cache it

# Override upstream headers
http-response set-header Cache-Control "public, max-age=60" if is_cacheable

# Check if Set-Cookie is preventing caching
# HAProxy won't cache responses with Set-Cookie headers by default
curl -I http://192.168.1.10:8080/api/v1/products | grep Set-Cookie

Cache configuration fails validation:

# Validate config
sudo haproxy -c -f /etc/haproxy/haproxy.cfg

# Common error: cache section must be before frontend/backend that references it
# Move cache section above frontend/backend definitions

High memory usage from cache:

# Check cache memory usage
echo "show cache" | sudo socat stdio /run/haproxy/admin.sock

# Reduce total-max-size
# cache my_cache
#   total-max-size 256

# Restart HAProxy to apply
sudo systemctl reload haproxy

Conclusion

HAProxy's built-in HTTP cache is a practical solution for reducing backend load when HAProxy is already in your stack as a load balancer. It works best for public, read-heavy API endpoints and static assets with proper Cache-Control headers. For large-scale caching or more granular purging control, supplement HAProxy with a dedicated cache layer like Varnish or Nginx proxy cache. Monitor hit rates via the stats socket to ensure your cache configuration is delivering measurable improvements.