Proxy Cache

Proxy Cache #

Caching di lapisan proxy adalah salah satu cara paling efektif untuk meningkatkan performa aplikasi web secara dramatis — response yang di-cache bisa dilayani langsung oleh proxy tanpa perlu menghubungi backend sama sekali. Ini mengurangi latensi, menghemat resource backend, dan meningkatkan kemampuan handling traffic spike.

Penting dipahami bahwa Caddy memiliki dua layer caching yang berbeda:

Layer 1: Static file caching (built-in)
  - Untuk file statis yang disajikan via file_server
  - Via ETag dan Cache-Control yang otomatis diset
  - Cache terjadi di sisi browser/CDN, bukan di Caddy

Layer 2: Reverse proxy response caching
  - Untuk response dari backend (dynamic content, API)
  - Membutuhkan plugin cache-handler (tidak built-in)
  - Cache terjadi di Caddy — backend tidak dihubungi untuk cache hit

Built-in: Cache Headers untuk Static Files #

Caddy secara otomatis menangani caching browser via header HTTP:

example.com {
    root * /var/www/html
    
    # Aset dengan hash di nama (immutable — cache selamanya)
    @hashed path_regexp \.[a-f0-9]{8,}\.(js|css|woff2?)$
    header @hashed Cache-Control "public, max-age=31536000, immutable"
    
    # Gambar (cache 30 hari)
    @images path *.jpg *.jpeg *.png *.gif *.webp *.svg *.ico *.avif
    header @images Cache-Control "public, max-age=2592000"
    
    # HTML dan root — jangan cache (agar versi terbaru selalu dimuat)
    @html {
        path *.html
        path /
    }
    header @html Cache-Control "no-cache, must-revalidate"
    
    # ETag otomatis di-generate oleh Caddy berdasarkan file content
    # Browser akan kirim If-None-Match di request berikutnya
    # Caddy return 304 Not Modified jika file tidak berubah → hemat bandwidth
    
    encode gzip zstd
    file_server
}

Bagaimana ETag Bekerja #

Request pertama:
  GET /app.a1b2c3.js
  
  Response:
  200 OK
  ETag: "abc123"
  Cache-Control: public, max-age=31536000, immutable
  Content-Length: 45678

Browser menyimpan di cache lokal.

Request kedua (besok, setelah max-age habis atau manual refresh):
  GET /app.a1b2c3.js
  If-None-Match: "abc123"      ← Browser kirim ETag yang disimpan
  
  Jika file tidak berubah:
  304 Not Modified              ← Tidak ada body! Hemat bandwidth
  
  Jika file berubah:
  200 OK
  ETag: "xyz789"               ← ETag baru
  (body baru dikirim)

Cache-Control Headers — Panduan Lengkap #

Direktif Cache-Control untuk Response:

no-store          → Jangan simpan di cache sama sekali
                   Untuk: data sensitif, real-time data

no-cache          → Simpan di cache, tapi selalu validasi ke server
                   Server bisa return 304 jika tidak berubah
                   Untuk: HTML halaman (agar update langsung terlihat)

public            → Bisa di-cache oleh siapa saja (browser, CDN, proxy)
private           → Hanya bisa di-cache oleh browser user (bukan CDN)
                   Untuk: response yang personal (user profile, cart)

max-age=N         → Cache berlaku N detik dari sekarang
s-maxage=N        → Override max-age khusus untuk shared cache (CDN, proxy)
                   Browser tetap pakai max-age

immutable         → File tidak akan berubah selama max-age berlaku
                   Browser tidak perlu validasi ulang
                   Untuk: hashed assets yang tidak pernah berubah

must-revalidate   → Setelah stale, HARUS validasi ke server
stale-while-revalidate=N  → Boleh sajikan konten stale sambil fetch yang baru
stale-if-error=N  → Boleh sajikan stale jika server error

Plugin cache-handler untuk Dynamic Content #

Untuk caching response dari backend (reverse proxy caching), kamu perlu plugin cache-handler yang harus di-compile dengan xcaddy:

# Build Caddy dengan cache-handler plugin
xcaddy build --with github.com/caddyserver/cache-handler

Konfigurasi Dasar cache-handler #

{
    # Aktifkan cache-handler di global level
    cache {
        # Backend storage untuk cache
        # Default: memory (cocok untuk development, tidak persistent)
        
        # Untuk production dengan data besar, gunakan Redis (butuh plugin tambahan)
        # backends {
        #     redis {
        #         host     localhost
        #         port     6379
        #         password {env.REDIS_PASSWORD}
        #     }
        # }
    }
}

example.com {
    cache {
        # Aturan caching
        default_cache_control "public, max-age=3600"
        
        # TTL default untuk response yang tidak punya Cache-Control
        ttl 1h
    }
    
    reverse_proxy backend:3000
}

Aturan Cache per Path #

{
    cache
}

api.example.com {
    # Konfigurasi cache berbeda per endpoint
    
    @static path /api/v1/categories /api/v1/countries /api/v1/languages
    @dynamic path /api/v1/users/* /api/v1/orders/*
    @realtime path /api/v1/prices/* /api/v1/inventory/*
    
    handle @static {
        cache {
            # Data statis — cache lama
            ttl 24h
            default_cache_control "public, max-age=86400"
        }
        reverse_proxy backend:8080
    }
    
    handle @dynamic {
        cache {
            # Data user — cache singkat, hanya untuk user yang sama
            ttl 5m
            default_cache_control "private, max-age=300"
        }
        reverse_proxy backend:8080
    }
    
    handle @realtime {
        # Data real-time — jangan cache sama sekali
        reverse_proxy backend:8080
    }
    
    handle {
        cache {
            ttl 1h
        }
        reverse_proxy backend:8080
    }
}

Cache Invalidation #

Salah satu tantangan terbesar caching adalah invalidation — memastikan konten yang sudah diupdate tidak dilayani dari cache lama:

Strategi 1: Cache Busting via URL Hash #

Ini adalah strategi yang paling reliable untuk aset statis:

# Build tool (Vite, Webpack, dll.) otomatis membuat nama file dengan hash
# app.js → app.a1b2c3d4.js
# Ketika konten berubah, hash berubah → URL baru → tidak ada cache lama

# Caddy meng-cache file dengan nama berbeda → tidak ada masalah invalidation

Strategi 2: Vary Header #

api.example.com {
    reverse_proxy backend:8080 {
        # Instruksikan cache untuk menyimpan versi berbeda
        # berdasarkan header Accept-Language
        header_down Vary "Accept-Language"
        
        # Cache terpisah untuk mobile vs desktop
        # header_down Vary "User-Agent"  ← Tidak disarankan (terlalu banyak variasi)
    }
}

Strategi 3: Surrogate Keys (dengan plugin) #

# Plugin seperti Souin mendukung surrogate keys untuk batch invalidation
# Setiap response di-tag, dan kamu bisa invalidate semua cache dengan tag tertentu

api.example.com {
    cache {
        # Tag cache berdasarkan entitas
        # Header Surrogate-Key dari backend: "product:123 category:electronics"
    }
    
    reverse_proxy backend:8080
}

# Invalidate via API:
# curl -X PURGE -H "Surrogate-Key: product:123" https://api.example.com/

CDN di Depan Caddy #

Untuk traffic skala besar, sering digunakan CDN (Cloudflare, CloudFront, Fastly) di depan Caddy:

Browser → CDN (Cloudflare) → Caddy → Backend

Cache layer:
1. CDN cache (edge, closest to user)
2. Caddy cache (origin, sebelum backend)
3. Browser cache (local)
{
    # IP Cloudflare sebagai trusted proxy
    servers {
        trusted_proxies static 103.21.244.0/22 103.22.200.0/22 ...
    }
}

example.com {
    # Set header untuk instruksi CDN
    header {
        # CDN cache 1 jam, browser cache 5 menit
        Cache-Control "public, max-age=300, s-maxage=3600"
        
        # Cloudflare Surrogate-Control untuk override
        Surrogate-Control "max-age=86400"
        
        # Tag untuk Cloudflare cache purge
        # Cache-Tag "product-list homepage"
    }
    
    reverse_proxy backend:3000
}

Monitoring Cache Performance #

# Dengan plugin cache-handler, status cache bisa dilihat di headers
curl -I https://api.example.com/v1/products
# Header yang perlu dicek:
# X-Cache: HIT atau MISS
# X-Cache-Hits: 5 (berapa kali di-hit)
# Age: 300 (berapa detik sudah di-cache)

# Metrics via Admin API
curl http://localhost:2019/metrics | grep cache

# Hitung cache hit rate dari access log
sudo cat /var/log/caddy/access.log | jq -r '.["resp_headers"]["X-Cache"][0]' \
    | sort | uniq -c

# Output:
# 1523 HIT
#  347 MISS
# Hit rate: 1523/(1523+347) = 81.4%

Pola Anti-Cache untuk Debugging #

Saat debugging, terkadang kamu perlu bypass cache:

example.com {
    # Jangan cache jika ada header X-No-Cache
    @noCache header X-No-Cache true
    
    handle @noCache {
        # Langsung ke backend tanpa cache
        reverse_proxy backend:3000
    }
    
    # Request normal dengan cache
    handle {
        cache {
            ttl 1h
        }
        reverse_proxy backend:3000
    }
}
# Test tanpa cache
curl -H "X-No-Cache: true" https://example.com/api/products

Ringkasan #

  • Caddy secara otomatis mengelola ETag dan If-None-Match untuk file statis — browser menerima 304 Not Modified jika file tidak berubah, hemat bandwidth.
  • Untuk aset statis (JS, CSS), gunakan strategi cache busting via hash di nama file + Cache-Control: immutable untuk performa optimal.
  • Caching response dari backend (proxy cache) membutuhkan plugin cache-handler — tidak built-in di Caddy standar.
  • Gunakan Cache-Control: no-cache (bukan no-store) untuk HTML — browser tetap menyimpan cache tapi selalu validasi ke server, server bisa return 304 untuk hemat bandwidth.
  • Untuk data sensitif/personal, gunakan Cache-Control: private agar CDN tidak meng-cache response yang seharusnya berbeda per user.
  • s-maxage di Cache-Control memungkinkan kamu mengatur TTL berbeda untuk CDN vs browser dalam satu header.

← Sebelumnya: Transport   Berikutnya: Round Robin →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact