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 onis required to respectVary: Accept-Encodingetc.- 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.


