Melindungi API dari banjir request: rate limiting dengan slowapi dan token bucket, throttling per-IP dan per-user, retry-after header yang benar, serta konfigurasi reverse proxy untuk pertahanan DoS berlapis.

Setelah di episode 18 pertahanan aplikasi sudah berlapis (input, query, auth, headers), sekarang kita hadapi serangan yang paling membosankan dan paling sering terjadi: banjir request. Rate limiting membatasi berapa banyak request yang bisa dipakai klien dalam satu jendela waktu — dan itu melindungi API dari serangan brute-force (episode 12), scraping agresif, dan DoS sederhana.
Mengapa episode ini penting? Endpoint publik tanpa rate limit adalah pintu terbuka: attacker bisa memanggil /token jutaan kali untuk brute-force password, atau menguras database dengan request tak henti. Rate limiting adalah pengaman murah yang berdampak besar pada stabilitas dan biaya.
Memahami mekanismenya membantu memilih angka yang tepat:
Analoginya: ember berisi token. Setiap request mengambil satu token; ember mengisi kembali dengan kecepatan tetap. Saat ember kosong → request ditolak. Inilah token bucket — memungkinkan lonjakan sesaat (burst) namun rata-rata tetap terkendali.
pip install slowapislowapi adalah library rate limiting populer untuk FastAPI — berbasis token bucket (limits) dan terintegrasi dengan dependency injection.
from fastapi import FastAPI, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi.responses import JSONResponse
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
@app.exception_handler(RateLimitExceeded)
async def rate_limit_exceeded_handler(
request: Request, exc: RateLimitExceeded
) -> JSONResponse:
return JSONResponse(
status_code=429,
content={"detail": "Terlalu banyak request, coba lagi nanti"},
headers={"Retry-After": str(exc.retry_after)},
)
@app.get("/items/")
@limiter.limit("10/minute")
def list_items(request: Request) -> dict:
return {"message": "OK"}Dua hal wajib di setiap setup slowapi:
app.state.limiter = limiter — slowapi membaca limiter dari app.state.@limiter.limit(...) mensyaratkan parameter request: Request di handler — inilah sumber identitas klien.Retry-After di header memberi tahu klien kapan boleh mencoba lagi — etika HTTP yang membuat klien pintar tidak membanjiri API.
Important
get_remote_address mengambil IP dari request. Jika aplikasi di belakang proxy (episode 24), IP yang terlihat adalah IP proxy — konfigurasi ProxyHeadersMiddleware / forwarded-allow-ips wajib agar rate limit per-IP bekerja. Kalau tidak, semua user akan dihitung sebagai satu IP.
Untuk endpoint yang butuh auth, identitas rate limit sebaiknya user (bukan IP) — IP di jaringan kantor bersama akan memblokir semua orang:
from typing import Annotated
from fastapi import Depends
from slowapi import Limiter
from app.deps import CurrentUser
limiter = Limiter(key_func=get_remote_address)
def get_user_key(request: Request) -> str:
# user dari dependency; fallback IP jika belum login
return str(getattr(request.state, "user_id", get_remote_address(request)))
@app.get("/me/data/")
@limiter.limit("30/minute")
def my_data(
request: Request,
current_user: CurrentUser,
) -> dict[str, str]:
request.state.user_id = current_user.id
return {"user": current_user.username}Catatan penting: key_func ditentukan sebelum request diproses, jadi dependency yang mengisi request.state harus berjalan lebih dulu — urutkan dengan hati-hati.
Kombinasi batas umum di API produksi:
@app.post("/token")
@limiter.limit("5/minute") # ketat: anti brute-force login
def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
...
@app.get("/items/")
@limiter.limit("100/minute") # sedang: browsing normal
def list_items(request: Request) -> dict:
...
@app.get("/health")
@limiter.limit("1000/minute") # longgar: ops harus lancar
def health(request: Request) -> dict:
...Aturan praktis: endpoint mahal (login, upload, AI inference) diberi batas ketat; endpoint murah (health check) diberi batas longgar.
Rate limiting di aplikasi hanyalah satu lapis. Serangan DoS sering ditahan di lapisan sebelum aplikasi — reverse proxy (Nginx) atau CDN:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
listen 80;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 30s;
proxy_connect_timeout 5s;
}
}limit_req menolak request melebihi 10 r/s per IP di lapisan proxy — aplikasi tidak pernah melihat banjir.proxy_read_timeout memutus koneksi yang menggantung, mencegah klien lambat menguras thread/worker.Tip
Pertahanan DoS berlapis: CDN untuk DDoS volume besar, Nginx untuk rate limit per-IP, slowapi untuk aturan bisnis per-user/endpoint. Jangan mencoba menggantikan satu lapis dengan yang lain — masing-masing bekerja di skala berbeda.
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/items/
doneOutput awal 200 terus-menerus lalu berubah menjadi 429 setelah batas 10 request — tanda rate limit bekerja. Di episode 25 kita akan melakukan load test yang lebih serius.
| Pitfall | Solusi |
|---|---|
| Rate limit dihitung per-IP di belakang proxy | ProxyHeadersMiddleware / forwarded-allow-ips |
Lupa request: Request di handler | slowapi error saat dekorasi |
Limiter tidak di-app.state | Rate limit tidak aktif |
| Batas terlalu ketat untuk user sah | Buat batas per endpoint yang realistis |
| Mengandalkan aplikasi saja untuk DDoS | Lapis di Nginx/CDN juga |
Inti yang harus dibawa pulang:
@limiter.limit.Retry-After agar klien bisa mundur dengan sopan.Di episode 20 selanjutnya kita akan membahas pagination, filtering & versioning — endpoint list production-grade dengan limit/offset dan cursor pagination, filter dinamis, serta strategi API versioning via path dan header. API kalian akan siap dipakai dalam skala besar!