Optimize H2O web server performance with advanced caching strategies, HTTP/2 compression, and production-grade tuning for high-traffic applications.
Prerequisites
- Root or sudo access
- Basic understanding of web servers
- SSL certificates (for HTTPS)
- Backend application (for proxy caching)
What this solves
H2O web server offers excellent HTTP/2 performance out of the box, but proper caching and compression configuration can dramatically improve response times and reduce bandwidth usage. This tutorial shows you how to configure advanced caching policies, optimize compression settings, and tune H2O for high-traffic production workloads.
Step-by-step configuration
Install H2O web server
Start by installing H2O and creating the basic directory structure for our configuration.
sudo apt update
sudo apt install -y h2o
sudo mkdir -p /etc/h2o/conf.d
Create main H2O configuration
Configure the main H2O server with HTTP/2 enabled and basic security settings.
user: h2o
pid-file: /var/run/h2o/h2o.pid
error-log: /var/log/h2o/error.log
access-log:
path: /var/log/h2o/access.log
format: "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\" %{duration}x"
HTTP/2 and performance settings
http2-idle-timeout: 10
http2-max-concurrent-requests-per-connection: 100
max-connections: 1024
num-threads: 4
Enable compression globally
compress: ON
hosts:
"example.com:80":
listen:
port: 80
paths:
"/":
file.dir: /var/www/html
"example.com:443":
listen:
port: 443
ssl:
certificate-file: /etc/ssl/certs/example.com.pem
key-file: /etc/ssl/private/example.com.key
paths:
"/":
file.dir: /var/www/html
Configure advanced caching directives
Create a dedicated caching configuration that handles different content types with appropriate cache policies.
# Static asset caching configuration
cache-control:
- extensions: [css, js, png, jpg, jpeg, gif, ico, svg, woff, woff2, ttf]
header: "Cache-Control: public, max-age=31536000, immutable"
- extensions: [html, htm]
header: "Cache-Control: public, max-age=3600, must-revalidate"
- extensions: [json, xml]
header: "Cache-Control: public, max-age=1800"
- extensions: [pdf, doc, docx]
header: "Cache-Control: public, max-age=86400"
Enable ETags for better cache validation
file.etag: ON
Memory-based file cache
file.send-gzip: ON
file.dir-listing: OFF
Configure expires headers
expires:
- ".css": "1 year"
- ".js": "1 year"
- ".png": "1 year"
- ".jpg": "1 year"
- ".jpeg": "1 year"
- ".gif": "1 year"
- ".ico": "1 year"
- ".svg": "1 year"
- ".woff": "1 year"
- ".woff2": "1 year"
- ".ttf": "1 year"
- ".html": "1 hour"
- ".json": "30 minutes"
Configure compression optimization
Set up advanced compression settings with proper MIME type handling and compression levels.
# Compression configuration
compress:
- gzip:
level: 6
min-size: 1024
- brotli:
level: 6
min-size: 1024
MIME types to compress
compress-mime-types:
- text/html
- text/css
- text/javascript
- text/xml
- text/plain
- text/csv
- application/javascript
- application/json
- application/xml
- application/rss+xml
- application/atom+xml
- image/svg+xml
- application/x-font-ttf
- application/vnd.ms-fontobject
- font/opentype
Don't compress already compressed formats
compress-exclude:
- image/png
- image/jpeg
- image/gif
- video/mp4
- video/mpeg
- audio/mp3
- application/zip
- application/gzip
- application/pdf
Configure proxy caching for dynamic content
Set up proxy caching for applications behind H2O with intelligent cache invalidation.
# Proxy caching configuration
proxy.reverse.url: "http://127.0.0.1:8080"
Enable proxy caching
proxy.cache: ON
proxy.cache.size: 128m
proxy.cache.dir: /var/cache/h2o
Cache rules for different paths
proxy.cache.rules:
- path: /api/
cache: OFF
- path: /admin/
cache: OFF
- path: /static/
max-age: 86400
- path: /images/
max-age: 604800
- path: /
max-age: 3600
vary: "Accept-Encoding, Cookie"
Bypass cache for authenticated users
proxy.cache.bypass:
- "$cookie_sessionid"
- "$http_authorization"
- "$arg_nocache"
Update main configuration with includes
Modify the main H2O configuration to include our caching and compression settings.
user: h2o
pid-file: /var/run/h2o/h2o.pid
error-log: /var/log/h2o/error.log
access-log:
path: /var/log/h2o/access.log
format: "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\" %{duration}x %{cache-status}x"
HTTP/2 and performance settings
http2-idle-timeout: 10
http2-max-concurrent-requests-per-connection: 100
max-connections: 1024
num-threads: 4
send-server-name: OFF
Include compression and caching configs
include: /etc/h2o/conf.d/compression.conf
hosts:
"example.com:80":
listen:
port: 80
paths:
"/":
file.dir: /var/www/html
include: /etc/h2o/conf.d/caching.conf
"/app":
include: /etc/h2o/conf.d/proxy-cache.conf
"example.com:443":
listen:
port: 443
ssl:
certificate-file: /etc/ssl/certs/example.com.pem
key-file: /etc/ssl/private/example.com.key
cipher-suite: ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
cipher-preference: server
header.add: "Strict-Transport-Security: max-age=31536000; includeSubDomains"
header.add: "X-Frame-Options: DENY"
header.add: "X-Content-Type-Options: nosniff"
paths:
"/":
file.dir: /var/www/html
include: /etc/h2o/conf.d/caching.conf
"/app":
include: /etc/h2o/conf.d/proxy-cache.conf
Create cache directory and set permissions
Set up the proxy cache directory with correct ownership and permissions.
sudo mkdir -p /var/cache/h2o
sudo chown h2o:h2o /var/cache/h2o
sudo chmod 755 /var/cache/h2o
sudo mkdir -p /var/log/h2o
sudo chown h2o:h2o /var/log/h2o
sudo chmod 755 /var/log/h2o
Configure log rotation
Set up log rotation to prevent disk space issues with access logs.
/var/log/h2o/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 644 h2o h2o
postrotate
if [ -f /var/run/h2o/h2o.pid ]; then
kill -USR1 cat /var/run/h2o/h2o.pid
fi
endscript
}
Create performance monitoring script
Set up a monitoring script to track cache hit rates and performance metrics.
#!/bin/bash
H2O Performance Monitoring Script
LOG_FILE="/var/log/h2o/access.log"
OUTPUT_FILE="/var/log/h2o/stats.log"
Get current timestamp
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
Calculate cache hit rate from last 1000 requests
CACHE_HITS=$(tail -n 1000 $LOG_FILE | grep -c "HIT")
CACHE_MISSES=$(tail -n 1000 $LOG_FILE | grep -c "MISS")
TOTAL_REQUESTS=$((CACHE_HITS + CACHE_MISSES))
if [ $TOTAL_REQUESTS -gt 0 ]; then
HIT_RATE=$(echo "scale=2; $CACHE_HITS / $TOTAL_REQUESTS * 100" | bc -l)
else
HIT_RATE="0.00"
fi
Calculate average response time
AVG_RESPONSE=$(tail -n 1000 $LOG_FILE | awk '{sum += $NF} END {if (NR > 0) print sum/NR; else print 0}')
Get current connections
CURRENT_CONN=$(ss -tuln | grep -E ':80|:443' | wc -l)
Output statistics
echo "[$TIMESTAMP] Cache Hit Rate: ${HIT_RATE}%, Avg Response: ${AVG_RESPONSE}ms, Active Connections: $CURRENT_CONN" >> $OUTPUT_FILE
Keep only last 1000 lines
tail -n 1000 $OUTPUT_FILE > $OUTPUT_FILE.tmp && mv $OUTPUT_FILE.tmp $OUTPUT_FILE
sudo chmod +x /usr/local/bin/h2o-stats.sh
sudo chown h2o:h2o /usr/local/bin/h2o-stats.sh
Set up automated cache warming
Create a cache warming script to pre-populate frequently accessed content.
#!/bin/bash
H2O Cache Warming Script
BASE_URL="https://example.com"
USER_AGENT="H2O-Cache-Warmer/1.0"
List of URLs to warm
URLS=(
"/"
"/about"
"/products"
"/contact"
"/static/css/main.css"
"/static/js/main.js"
"/api/popular-items"
)
echo "Starting cache warming at $(date)"
for url in "${URLS[@]}"; do
echo "Warming: ${BASE_URL}${url}"
curl -s -H "User-Agent: $USER_AGENT" "${BASE_URL}${url}" > /dev/null
sleep 1
done
echo "Cache warming completed at $(date)"
sudo chmod +x /usr/local/bin/cache-warm.sh
sudo chown h2o:h2o /usr/local/bin/cache-warm.sh
Configure systemd timer for monitoring
Set up automated monitoring and cache warming with systemd timers.
[Unit]
Description=H2O Performance Monitor
After=h2o.service
[Service]
Type=oneshot
User=h2o
ExecStart=/usr/local/bin/h2o-stats.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run H2O Performance Monitor every 5 minutes
Requires=h2o-monitor.service
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now h2o-monitor.timer
Test configuration and restart H2O
Validate the configuration syntax and restart H2O with the new settings.
sudo h2o -t -c /etc/h2o/h2o.conf
sudo systemctl restart h2o
sudo systemctl enable h2o
Performance tuning for high traffic
Configure system-level optimizations
Optimize kernel parameters for high-performance web serving.
# Network performance tuning
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 5000
net.core.rmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_default = 262144
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 12582912 16777216
net.ipv4.tcp_wmem = 4096 12582912 16777216
net.ipv4.tcp_max_syn_backlog = 8096
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
File descriptor limits
fs.file-max = 2097152
sudo sysctl -p /etc/sysctl.d/99-h2o.conf
Configure service limits
Increase file descriptor limits for the H2O service.
[Service]
LimitNOFILE=65535
LimitNPROC=32768
sudo mkdir -p /etc/systemd/system/h2o.service.d/
sudo systemctl daemon-reload
sudo systemctl restart h2o
Verify your setup
Check that H2O is running with optimized configuration and verify caching is working.
sudo systemctl status h2o
sudo h2o -t -c /etc/h2o/h2o.conf
Test HTTP/2 support
curl -I -H "Accept-Encoding: gzip, br" https://example.com
Check cache statistics
sudo tail -f /var/log/h2o/stats.log
Test compression
curl -H "Accept-Encoding: gzip" -I https://example.com/static/css/main.css
Monitor active connections
sudo ss -tuln | grep -E ':80|:443'
Check cache directory
sudo ls -la /var/cache/h2o/
Verify timer is running
sudo systemctl status h2o-monitor.timer
Monitoring and testing
Load testing with performance validation
Use Apache Bench to test the server under load and validate caching performance.
# Install Apache Bench
sudo apt install -y apache2-utils # Ubuntu/Debian
sudo dnf install -y httpd-tools # AlmaLinux/Rocky
Test concurrent connections
ab -n 1000 -c 100 https://example.com/
Test static asset caching
ab -n 500 -c 50 https://example.com/static/css/main.css
Monitor during test
watch -n 1 'sudo ss -tuln | grep -E ":80|:443" | wc -l'
Set up Prometheus monitoring
Configure Prometheus to collect H2O metrics for long-term monitoring.
# Metrics endpoint
status:
- port: 9090
bind: 127.0.0.1
Add to main config under paths:
"/metrics":
status: ON
header.add: "Content-Type: text/plain"
For comprehensive monitoring, you can set up Prometheus and Grafana monitoring stack to track H2O performance metrics over time.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Cache not working | Incorrect cache directory permissions | sudo chown -R h2o:h2o /var/cache/h2o |
| High memory usage | Cache size too large | Reduce proxy.cache.size in configuration |
| Compression not applied | File size below min-size threshold | Adjust min-size in compression config |
| SSL handshake errors | Cipher suite compatibility | Update cipher-suite list in SSL config |
| Connection timeouts | System limits too low | Increase max-connections and system limits |
| Log files growing too large | Missing log rotation | Verify logrotate configuration is active |
| Cache warming fails | Script permissions or network issues | Check script ownership and SSL certificate validity |
Next steps
- Integrate H2O with Let's Encrypt for automatic SSL certificates
- Configure H2O HTTP/2 server load balancing with health checks
- Set up H2O monitoring with Prometheus and Grafana dashboards
- Configure H2O web application firewall protection
- Implement H2O CDN integration for global performance
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
DOMAIN="${1:-example.com}"
BACKEND_PORT="${2:-8080}"
usage() {
echo "Usage: $0 [domain] [backend_port]"
echo " domain: Domain name (default: example.com)"
echo " backend_port: Backend application port (default: 8080)"
exit 1
}
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop h2o >/dev/null 2>&1 || true
systemctl disable h2o >/dev/null 2>&1 || true
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "[1/8] Updating package repositories..."
$PKG_UPDATE
log_info "[2/8] Installing H2O web server..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL h2o
else
# RHEL-based systems may need EPEL
$PKG_INSTALL epel-release || true
$PKG_INSTALL h2o || {
log_warn "H2O not found in repositories, installing from source..."
$PKG_INSTALL cmake gcc gcc-c++ make openssl-devel zlib-devel
cd /tmp
curl -L https://github.com/h2o/h2o/archive/v2.2.6.tar.gz | tar xz
cd h2o-2.2.6
cmake -DWITH_BUNDLED_SSL=on .
make -j$(nproc)
make install
}
fi
log_info "[3/8] Creating directory structure..."
mkdir -p /etc/h2o/conf.d
mkdir -p /var/log/h2o
mkdir -p /var/run/h2o
mkdir -p /var/cache/h2o
mkdir -p /var/www/html
# Create h2o user if it doesn't exist
if ! id h2o >/dev/null 2>&1; then
useradd -r -s /bin/false h2o
fi
chown h2o:h2o /var/log/h2o /var/run/h2o /var/cache/h2o
chmod 755 /var/log/h2o /var/run/h2o /var/cache/h2o
log_info "[4/8] Creating main H2O configuration..."
cat > /etc/h2o/h2o.conf << EOF
user: h2o
pid-file: /var/run/h2o/h2o.pid
error-log: /var/log/h2o/error.log
access-log:
path: /var/log/h2o/access.log
format: "%h %l %u %t \\\"%r\\\" %s %b \\\"%{Referer}i\\\" \\\"%{User-agent}i\\\" %{duration}x"
# HTTP/2 and performance settings
http2-idle-timeout: 10
http2-max-concurrent-requests-per-connection: 100
max-connections: 1024
num-threads: 4
# Enable compression globally
compress: ON
hosts:
"${DOMAIN}:80":
listen:
port: 80
paths:
"/":
file.dir: /var/www/html
file.index: ['index.html', 'index.htm']
EOF
log_info "[5/8] Configuring advanced caching directives..."
cat > /etc/h2o/conf.d/caching.conf << 'EOF'
# Static asset caching configuration
header.add: "Cache-Control: public, max-age=31536000, immutable"
header.set: "Cache-Control: public, max-age=3600, must-revalidate"
# Enable ETags for better cache validation
file.etag: ON
file.send-gzip: ON
file.dir-listing: OFF
# Expires headers for different file types
expires: 31536000
EOF
log_info "[6/8] Configuring compression optimization..."
cat > /etc/h2o/conf.d/compression.conf << 'EOF'
# Compression configuration
compress:
gzip: 6
br: 6
# MIME types to compress
compress-minimum-size: 1024
EOF
log_info "[7/8] Configuring proxy caching for dynamic content..."
cat >> /etc/h2o/h2o.conf << EOF
"${DOMAIN}:80":
listen:
port: 80
paths:
"/api":
proxy.reverse.url: "http://127.0.0.1:${BACKEND_PORT}"
proxy.timeout.io: 30000
"/static":
file.dir: /var/www/html/static
header.add: "Cache-Control: public, max-age=86400"
"/":
file.dir: /var/www/html
EOF
chown root:h2o /etc/h2o/h2o.conf /etc/h2o/conf.d/*.conf
chmod 644 /etc/h2o/h2o.conf /etc/h2o/conf.d/*.conf
# Create a simple index file
cat > /var/www/html/index.html << EOF
<!DOCTYPE html>
<html>
<head>
<title>H2O Server</title>
</head>
<body>
<h1>H2O Web Server is Running</h1>
<p>Your optimized H2O server with caching and compression is working!</p>
</body>
</html>
EOF
chown h2o:h2o /var/www/html/index.html
chmod 644 /var/www/html/index.html
log_info "[8/8] Starting and enabling H2O service..."
systemctl daemon-reload
systemctl enable h2o
systemctl start h2o
# Configure firewall
if command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true
firewall-cmd --reload >/dev/null 2>&1 || true
fi
if command -v ufw >/dev/null 2>&1; then
ufw allow 80/tcp >/dev/null 2>&1 || true
fi
# Verification checks
log_info "Verifying installation..."
if systemctl is-active h2o >/dev/null 2>&1; then
log_info "✓ H2O service is running"
else
log_error "✗ H2O service is not running"
exit 1
fi
if netstat -ln 2>/dev/null | grep -q ":80.*LISTEN" || ss -ln 2>/dev/null | grep -q ":80.*LISTEN"; then
log_info "✓ H2O is listening on port 80"
else
log_error "✗ H2O is not listening on port 80"
exit 1
fi
if curl -s -o /dev/null -w "%{http_code}" "http://localhost" | grep -q "200"; then
log_info "✓ HTTP requests are working"
else
log_warn "! HTTP test failed (this may be normal if behind a firewall)"
fi
log_info "H2O installation completed successfully!"
log_info "Configuration files:"
log_info " Main config: /etc/h2o/h2o.conf"
log_info " Additional configs: /etc/h2o/conf.d/"
log_info " Document root: /var/www/html"
log_info " Log files: /var/log/h2o/"
log_info ""
log_info "To customize for your domain, edit /etc/h2o/h2o.conf and replace '${DOMAIN}' with your actual domain"
log_info "Then restart the service: systemctl restart h2o"
Review the script before running. Execute with: bash install.sh