Setiap API publik punya musuh yang sama: permintaan berlebihan. Bot yang scraping endpoint berulang-ulang, percobaan brute force di halaman login, atau klien yang bug dan mengirim request dalam loop tanpa henti. Kalau kamu sudah membangun API dengan FastAPI, menambahkan lapisan rate limiting cuma butuh sekitar sepuluh menit. Kita pakai slowapi, library yang dibuat khusus untuk pekerjaan ini. Kalau kamu belum kenal framework-nya, saya sudah membahas dasarnya di Cara Membuat REST API dengan FastAPI.

Siapkan Proyek dan Install slowapi

Mulai dari proyek kosong. Buat virtual environment, lalu install tiga paket: fastapi, uvicorn untuk menjalankan server, dan slowapi untuk rate limiting.

bash
python -m venv .venv
source .venv/bin/activate
pip install "fastapi" "uvicorn[standard]" "slowapi"

Limiter Default untuk Semua Endpoint

slowapi bekerja lewat objek Limiter. Setiap request yang masuk dihitung berdasarkan sebuah key — secara default alamat IP si pengirim. Kamu bisa pasang batas default yang berlaku untuk seluruh endpoint, lalu menimpa batas itu untuk endpoint tertentu.

from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

limiter = Limiter(
    key_func=get_remote_address,
    default_limits=["200/day", "50/hour"],
)

app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/search")
def search(q: str = ""):
    return {"query": q}

Tiga baris penting di sini: app.state.limiter menyimpan instance Limiter supaya bisa dipakai di mana saja, lalu app.add_exception_handler menangkap RateLimitExceeded dan mengubahnya jadi respons 429. Dengan default_limits di atas, setiap IP cuma boleh 50 request per jam dan 200 per hari ke semua endpoint sekaligus.

Batas default berlaku kumulatif untuk seluruh endpoint, bukan per endpoint. Kalau kamu ingin tiap endpoint punya jatah sendiri, pakai decorator @limiter.limit seperti di bagian berikutnya.

Batas Khusus per Endpoint

Endpoint tertentu sering dapat trafik lebih tinggi daripada yang lain. Endpoint login, misalnya, harus dibatasi jauh lebih ketat karena jadi sasaran brute force. Masih di main.py yang sama, pakai decorator @limiter.limit untuk menimpa batas default.

python
from fastapi import Request


@app.get("/login")
@limiter.limit("5/minute")
def login(request: Request):
    return {"message": "form login"}


@app.get("/api/expensive")
@limiter.limit("10/minute;100/hour")
def expensive(request: Request):
    return {"data": "hasil proses berat"}

Perhatikan dua hal. Pertama, endpoint yang diberi decorator @limiter.limit wajib punya parameter request: Request — slowapi membacanya untuk mengambil alamat IP. Kedua, kamu bisa menumpuk beberapa batas sekaligus dengan titik koma: 10/minute;100/hour artinya maksimal 10 request per menit dan 100 per jam.

Key Kustom: Bukan Cuma IP

Rate limiting berbasis IP sudah cukup untuk banyak kasus. Tapi kalau API-mu memakai API key atau token autentikasi, lebih masuk akal membatasi berdasarkan key itu — supaya satu pengguna tidak lolos hanya karena ganti-ganti IP. Tinggal ganti key_func.

python
def key_func(request: Request):
    api_key = request.headers.get("X-API-Key")
    if api_key:
        return f"apikey:{api_key}"
    return get_remote_address(request)


limiter = Limiter(key_func=key_func, default_limits=["200/day", "50/hour"])

Dengan begini, pengguna yang memakai header X-API-Key dihitung berdasarkan key-nya, bukan IP-nya. Kalau kamu memakai JWT untuk autentikasi, subject token bisa jadi key — konsepnya sudah saya bahas di Autentikasi JWT di FastAPI.

Pesan 429 yang Ramah

Handler bawaan slowapi sudah mengembalikan 429 dengan detail yang cukup, tapi formatnya mungkin tidak cocok dengan standar respons API-mu. Ganti dengan handler sendiri supaya semua error konsisten.

python
from fastapi.responses import JSONResponse


async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return JSONResponse(
        status_code=429,
        content={
            "error": "rate_limited",
            "detail": "Terlalu banyak permintaan. Coba lagi beberapa saat.",
        },
    )


app.add_exception_handler(RateLimitExceeded, rate_limit_handler)

Simpan counter rate limit di memory kalau kamu cuma menjalankan satu proses. Begitu API-mu di-deploy dengan banyak worker (uvicorn --workers 4) atau beberapa container, memory lokal tiap proses tidak saling tahu — satu IP bisa lewat batas. Solusinya simpan counter di Redis.

Simpan Counter di Redis untuk Multi-Instance

slowapi mendukung Redis lewat storage_uri. Jalankan Redis dulu, lalu arahkan Limiter ke sana.

bash
docker run -d --name redis-rate -p 6379:6379 redis:7
python
limiter = Limiter(
    key_func=get_remote_address,
    default_limits=["200/day", "50/hour"],
    storage_uri="redis://localhost:6379/0",
)

Cukup satu baris tambahan. Semua instance API yang menunjuk ke Redis yang sama akan berbagi counter, jadi batas tetap konsisten mau dijalankan dengan berapa worker pun. Dasar-dasar Redis-nya sudah saya tulis di Belajar Redis dengan Python.

Uji Rate Limiter

Jalankan server, lalu tembak endpoint yang dibatasi lima kali per menit.

bash
uvicorn main:app --reload
bash
for i in $(seq 1 7); do
  curl -s -o /dev/null -w "request $i -> HTTP %{http_code}\n" http://127.0.0.1:8000/login
done

Lima request pertama mengembalikan 200, dua berikutnya 429. Tambahkan header Retry-After di handler kalau kamu ingin memberi tahu klien kapan boleh mencoba lagi.

Langkah selanjutnya yang masuk akal: pasang rate limiting di endpoint login dan endpoint publik lain di API yang sedang kamu kerjakan, lalu tes apakah batas per key (bukan per IP) lebih cocok untuk kasusmu.