Configure H2O HTTP/2 web server caching and compression optimization for high performance

Intermediate 45 min Apr 29, 2026 550 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -y
sudo dnf 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

SymptomCauseFix
Cache not workingIncorrect cache directory permissionssudo chown -R h2o:h2o /var/cache/h2o
High memory usageCache size too largeReduce proxy.cache.size in configuration
Compression not appliedFile size below min-size thresholdAdjust min-size in compression config
SSL handshake errorsCipher suite compatibilityUpdate cipher-suite list in SSL config
Connection timeoutsSystem limits too lowIncrease max-connections and system limits
Log files growing too largeMissing log rotationVerify logrotate configuration is active
Cache warming failsScript permissions or network issuesCheck script ownership and SSL certificate validity

Next steps

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

Rotterdam 01:26 · reachable in a message, no ticket form