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-Matchuntuk file statis — browser menerima304 Not Modifiedjika file tidak berubah, hemat bandwidth.- Untuk aset statis (JS, CSS), gunakan strategi cache busting via hash di nama file +
Cache-Control: immutableuntuk performa optimal.- Caching response dari backend (proxy cache) membutuhkan plugin
cache-handler— tidak built-in di Caddy standar.- Gunakan
Cache-Control: no-cache(bukanno-store) untuk HTML — browser tetap menyimpan cache tapi selalu validasi ke server, server bisa return304untuk hemat bandwidth.- Untuk data sensitif/personal, gunakan
Cache-Control: privateagar CDN tidak meng-cache response yang seharusnya berbeda per user.s-maxagediCache-Controlmemungkinkan kamu mengatur TTL berbeda untuk CDN vs browser dalam satu header.