Rate Limiting #
Rate limiting membatasi jumlah request yang bisa dilakukan oleh klien dalam periode waktu tertentu. Ini adalah lini pertahanan penting terhadap berbagai ancaman: brute force attack pada form login, abuse API oleh bot atau scraper, DDoS layer 7, dan penggunaan berlebihan yang merugikan pengguna lain.
Caddy tidak memiliki rate limiting bawaan — ini adalah keputusan desain yang disengaja agar core tetap ringan. Rate limiting tersedia melalui plugin caddy-ratelimit yang perlu dikompilasi bersama Caddy.
Instalasi Plugin Rate Limit #
# Build Caddy dengan plugin rate limiting
xcaddy build --with github.com/mholt/caddy-ratelimit
# Verifikasi plugin tersedia
./caddy list-modules | grep rate
# Output: http.handlers.rate_limit
# Replace binary yang ada
sudo systemctl stop caddy
sudo cp ./caddy /usr/bin/caddy
sudo systemctl start caddy
Konsep Dasar Rate Limiting #
Window-based rate limiting:
Tentukan: berapa request yang diizinkan dalam berapa detik?
Contoh: 100 request per menit per IP
Window sliding (lebih smooth):
T=0s IP-1 request ke-1
T=10s IP-1 request ke-50
T=59s IP-1 request ke-100 → limit akan tercapai
T=60s IP-1 request ke-101 → 429 Too Many Requests
(tapi 1 slot terbuka dari request di T=0s)
Window fixed (lebih simple):
T=0s - T=60s: IP-1 bisa 100 request
T=60s - T=120s: IP-1 bisa 100 request lagi (reset)
Konfigurasi Dasar #
example.com {
rate_limit {
zone dynamic {
# Key: apa yang di-track (IP address dalam contoh ini)
key {remote_host}
# Window: periode waktu
window 1m
# Events: jumlah request yang diizinkan dalam window
events 100
}
}
reverse_proxy localhost:3000
}
Rate Limiting per Endpoint #
Endpoint yang berbeda memerlukan limit yang berbeda:
api.example.com {
# Login endpoint — sangat ketat untuk cegah brute force
@login path /api/auth/login /api/auth/register
rate_limit @login {
zone login_zone {
key {remote_host}
window 15m
events 10 # Hanya 10 percobaan per 15 menit
}
}
# API endpoint umum — lebih longgar
@api path /api/*
rate_limit @api {
zone api_zone {
key {remote_host}
window 1m
events 60 # 60 request per menit (1 per detik rata-rata)
}
}
# Endpoint search — bisa cukup mahal di backend
@search path /api/search
rate_limit @search {
zone search_zone {
key {remote_host}
window 1m
events 10 # Hanya 10 search per menit
}
}
reverse_proxy backend:8080
}
Rate Limiting per User (Setelah Autentikasi) #
Untuk API dengan autentikasi, rate limiting berbasis user lebih adil dari berbasis IP:
api.example.com {
# Asumsi: backend menambahkan header X-User-ID setelah validasi JWT
# atau Caddy plugin auth mengekstrak user ID dari token
@authenticated header X-User-ID *
@unauthenticated not header X-User-ID *
# User terautentikasi: limit lebih longgar
rate_limit @authenticated {
zone authenticated_users {
key {header.X-User-ID}
window 1m
events 1000 # 1000 request per menit per user
}
}
# User tidak terautentikasi: limit lebih ketat
rate_limit @unauthenticated {
zone public_access {
key {remote_host}
window 1m
events 30 # Hanya 30 request per menit untuk akses publik
}
}
reverse_proxy backend:8080
}
Response Headers Rate Limit #
Memberikan informasi rate limit ke client memungkinkan mereka mengelola request dengan lebih baik:
api.example.com {
rate_limit {
zone api {
key {remote_host}
window 1m
events 100
}
}
# Tambahkan header informasi rate limit ke response
# Plugin rate_limit otomatis menambahkan:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 85
# X-RateLimit-Reset: 1704067200 (unix timestamp)
# Retry-After: 45 (hanya saat 429)
reverse_proxy backend:8080
}
Client bisa menggunakan header ini untuk menghindari rate limit:
// Client-side rate limit handling
async function apiRequest(url) {
const response = await fetch(url);
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = parseInt(response.headers.get('X-RateLimit-Reset'));
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After'));
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
await sleep(retryAfter * 1000);
return apiRequest(url); // Retry
}
// Slow down jika hampir mencapai limit
if (remaining < 10) {
const timeUntilReset = reset - Math.floor(Date.now() / 1000);
const delayPerRequest = (timeUntilReset * 1000) / remaining;
await sleep(delayPerRequest);
}
return response;
}
Distributed Rate Limiting dengan Redis #
Untuk deployment multi-instance, rate limit perlu di-share antar instance:
{
# Konfigurasi storage Redis untuk rate limit terdistribusi
}
api.example.com {
rate_limit {
distributed {
# Gunakan Redis sebagai shared storage
# (tergantung dukungan plugin yang digunakan)
backend redis {
host localhost
port 6379
password {env.REDIS_PASSWORD}
}
}
zone api_shared {
key {remote_host}
window 1m
events 100
}
}
reverse_proxy backend-1:8080 backend-2:8080 backend-3:8080
}
Rate Limiting di Tingkat Nginx/Caddy vs Backend #
Perbandingan pendekatan:
Di Caddy (layer proxy):
✓ Permintaan yang melewati limit TIDAK mencapai backend sama sekali
✓ Hemat resource backend
✓ Satu titik konfigurasi untuk semua service di balik Caddy
✗ Tidak aware konteks aplikasi (user tier, subscription level, dll.)
✗ Tidak bisa diferensiasi berdasarkan data yang hanya ada di aplikasi
Di Backend (aplikasi):
✓ Bisa rate limit berdasarkan konteks aplikasi (user tier, quota)
✓ Bisa custom response message yang lebih informatif
✓ Bisa exempt pengguna premium
✗ Request tetap sampai ke backend sebelum ditolak (buang resource)
✗ Perlu implementasi di setiap service
Rekomendasi: Lakukan KEDUANYA
→ Caddy: rate limit kasar berbasis IP untuk proteksi DDoS/abuse
→ Backend: rate limit halus berbasis user/subscription untuk billing
Rate Limiting Alternatif: Fail2ban #
Jika tidak ingin menggunakan plugin, Fail2ban bisa membaca log Caddy dan memblokir IP yang berulang kali mendapat error:
# Install fail2ban
sudo apt install fail2ban
# Konfigurasi jail untuk Caddy
# /etc/fail2ban/jail.d/caddy.conf
cat << 'EOF' | sudo tee /etc/fail2ban/jail.d/caddy.conf
[caddy-auth]
enabled = true
port = http,https
logpath = /var/log/caddy/access.log
backend = auto
maxretry = 5
findtime = 600 ; 10 menit
bantime = 3600 ; 1 jam ban
[caddy-4xx]
enabled = true
port = http,https
logpath = /var/log/caddy/access.log
backend = auto
maxretry = 50
findtime = 60
bantime = 1800
EOF
# Filter untuk mendeteksi banyak 4xx dari IP yang sama
# /etc/fail2ban/filter.d/caddy-4xx.conf
cat << 'EOF' | sudo tee /etc/fail2ban/filter.d/caddy-4xx.conf
[Definition]
failregex = ^.*"remote_addr":"<HOST>:[0-9]+".*"status":(4[0-9]{2}),.*$
ignoreregex =
datepattern = %%Y-%%m-%%dT%%H:%%M:%%S
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status caddy-4xx
Throttling Upload dan Download Besar #
Untuk melindungi bandwidth dan server dari client yang mengunduh/mengunggah secara berlebihan:
files.example.com {
# Rate limit untuk endpoint upload
@upload path /upload/*
rate_limit @upload {
zone upload_zone {
key {remote_host}
window 1h
events 20 # Maksimal 20 upload per jam per IP
}
}
# Rate limit untuk download file besar
@download path /files/*
rate_limit @download {
zone download_zone {
key {remote_host}
window 10m
events 50 # 50 file download per 10 menit
}
}
file_server { root /var/www/files }
}
Ini penting untuk mencegah satu user menghabiskan bandwidth server, yang pada akhirnya mempengaruhi user lain.
Ringkasan #
- Rate limiting bawaan Caddy tidak ada — gunakan plugin
caddy-ratelimit(xcaddy build --with github.com/mholt/caddy-ratelimit).- Buat zona rate limit berbeda untuk endpoint yang berbeda: endpoint login/register jauh lebih ketat dari endpoint API umum.
- Rate limit berbasis user ID lebih adil dari berbasis IP untuk API dengan autentikasi — pengguna di belakang NAT tidak saling mengganggu kuota.
- Tambahkan
X-RateLimit-*headers ke response agar client bisa mengelola request mereka sendiri dan tidak buang-buang percobaan.- Untuk deployment multi-instance, rate limit harus distributed via Redis agar semua instance berbagi state yang sama.
- Kombinasikan rate limiting di Caddy (proteksi DDoS/abuse kasar) dengan rate limiting di backend (quota per user/subscription) untuk perlindungan berlapis.