Rate limiting membatasi berapa banyak request yang boleh dikirim satu pengirim dalam satu rentang waktu. Bayangin mesin kopi di kantor yang cuma mau mengeluarkan 5 gelas per 10 detik untuk satu kartu karyawan. Gelas keenam ditolak, dan layarnya bilang "coba lagi 9 detik lagi". Kartunya adalah kunci (IP atau user), 5 per 10 detik adalah batas, dan pesan di layar adalah header Retry-After.
🧪 Tebakan cepat: batasnya 5 request per 10 detik. Kamu mengirim 6 request dalam 1 detik. Request keenam dijawab status apa, dan berapa detik kamu harus menunggu? Tebak dulu, jawabannya ada di bagian "Yang dikirim server saat batas habis".
Kenapa API butuh batas
Tanpa batas, siapa pun bisa mengirim request sebanyak dan secepat yang dia mau:
- Brute force login: mencoba ribuan password per menit untuk satu akun.
- Credential stuffing: mencoba pasangan email dan password yang bocor dari situs lain, ribuan sekaligus.
- Scraping dan spam: menyedot semua data atau membanjiri form.
- DoS dan DDoS: membanjiri server dengan request sampai CPU, memori, atau bandwidth-nya habis, dan user asli gak terlayani. DoS (Denial of Service) datang dari satu sumber, DDoS (Distributed Denial of Service) dari banyak sumber sekaligus.
Rate limiting adalah satu lapisan pertahanan untuk semua itu. Untuk DDoS besar, dia dibantu lapisan lain seperti CDN dan firewall, karena request yang ditolak pun tetap sampai ke server.
Yang dikirim server saat batas habis
Ini jejak dari Lab: batas 5 per 10 detik, dihitung dengan sliding window (cara menghitungnya ada di bagian berikutnya). Waktu dihitung dari request pertama.
| Request | Waktu | Yang lolos di 10 detik terakhir | Jawaban server |
|---|---|---|---|
| 1 | 0,0 s | 0 | 200 OK, RateLimit-Remaining: 4 |
| 2 | 0,2 s | 1 | 200 OK, RateLimit-Remaining: 3 |
| 3 | 0,4 s | 2 | 200 OK, RateLimit-Remaining: 2 |
| 4 | 0,6 s | 3 | 200 OK, RateLimit-Remaining: 1 |
| 5 | 0,8 s | 4 | 200 OK, RateLimit-Remaining: 0 |
| 6 | 1,0 s | 5 | 429 Too Many Requests, Retry-After: 9 |
| 7 | 10,1 s | 4 (yang di 0,0 s sudah lewat 10 detik) | 200 OK, RateLimit-Remaining: 0 |
Request keenam dijawab 429 Too Many Requests dengan Retry-After: 9: request pertama (di 0,0 s) baru keluar dari jendela 10 detik pada 10,0 s, dan sekarang 1,0 s, jadi sisa 9 detik. Itu jawaban tebakannya.
429adalah status HTTP khusus untuk "terlalu banyak request".Retry-Afterberisi berapa detik client harus menunggu. Client yang baik membacanya dan diam dulu.RateLimit-LimitdanRateLimit-Remainingmemberi tahu batasnya dan sisa jatahnya, supaya client bisa memperlambat sebelum kena 429. Laravel memakai namaX-RateLimit-LimitdanX-RateLimit-Remaining.
Tiga cara menghitung
Contoh: batas 5 per 10 detik, lalu 5 request di detik 9,5 dan 5 request lagi di detik 10,5.
| Cara | Cara kerjanya | Di contoh tadi |
|---|---|---|
| Fixed window | Penghitung per jendela tetap (0 sampai 10 s, 10 sampai 20 s), kembali ke 0 tiap jendela baru | 10 lolos dalam 1 detik |
| Sliding window | Menghitung request yang lolos di 10 detik terakhir, dihitung dari sekarang | 5 lolos, 5 kena 429 |
| Token bucket | Ember isi 5 token, terisi 1 token tiap 2 detik. Tiap request memakai 1 token | 5 lolos, lalu 1 lolos tiap 2 detik |
- Fixed window paling sederhana, tapi punya celah di batas jendela: penghitungnya reset di detik 10, jadi 10 request bisa lolos dalam waktu sangat dekat.
- Sliding window menutup celah itu, karena jendelanya selalu "10 detik terakhir dari sekarang".
- Token bucket mengizinkan ledakan kecil (burst) sampai isi embernya, lalu laju yang rata. Di contoh tadi, pada 10,5 s ember baru terisi setengah token, jadi
Retry-After: 1, dan pada 11,5 s satu request lolos.
Kode sliding window di JavaScript
const LIMIT = 5
const WINDOW_MS = 10_000 // 10 detik
const hits = new Map() // kunci (IP atau user id) -> waktu request yang lolos
function rateLimit(key, now = Date.now()) {
const recent = (hits.get(key) ?? []).filter((t) => t > now - WINDOW_MS)
if (recent.length >= LIMIT) {
const retryAfter = Math.ceil((recent[0] + WINDOW_MS - now) / 1000)
return { status: 429, headers: { 'Retry-After': String(retryAfter) } }
}
recent.push(now)
hits.set(key, recent)
return { status: 200, headers: { 'RateLimit-Limit': String(LIMIT), 'RateLimit-Remaining': String(LIMIT - recent.length) } }
}
filter((t) => t > now - WINDOW_MS)membuang waktu yang sudah lebih tua dari 10 detik. Sisanya adalah request di jendela sekarang.recent.length >= LIMITberarti jatah habis: kembalikan 429 tanpa mencatat request ini.recent[0]adalah request tertua di jendela. Dia keluar dari jendela padarecent[0] + WINDOW_MS, jadi selisihnya dengannow, dibulatkan ke atas, adalahRetry-After.hitsdisimpan di memori satu proses. Kalau server-mu lebih dari satu, simpan hitungannya di Redis supaya semua server berbagi angka yang sama. Di Express, paketexpress-rate-limitsudah menyediakan semua ini.
Di Laravel
// routes/api.php: 60 request per 1 menit
Route::middleware('throttle:60,1')->group(function () {
Route::get('/data', [DataController::class, 'index']);
});
// AppServiceProvider::boot()
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:login');
throttle:60,1artinya 60 request per 1 menit.by(...)menentukan kunci penghitungnya.$request->user()?->id ?: $request->ip()artinya: kalau user sudah login, hitung per id user, kalau belum, per IP.- Endpoint login diberi batas jauh lebih ketat (5 per menit) daripada API biasa.
- Lewat batas, Laravel otomatis menjawab
429dengan headerRetry-After.
⚠️ Jebakan yang sering ketemu
- Batas yang sama untuk semua endpoint. Login, reset password, dan pembayaran butuh batas jauh lebih ketat daripada daftar produk.
- Cuma per IP. Satu kantor bisa berbagi satu IP (banyak user jujur ikut kena 429), dan penyerang bisa memakai banyak IP. Gabungkan per IP dengan per user, dan untuk login, per email yang dicoba.
- Hitungan di memori dengan banyak server. Tiap server menghitung sendiri, jadi batasnya berlipat. Pakai Redis.
- Lupa
Retry-After. Tanpa itu, client gak tahu kapan boleh mencoba lagi dan malah terus mengirim.
TL;DR: Rate limiting membatasi request per kunci (IP atau user id) per rentang waktu, untuk menahan brute force, credential stuffing, scraping, dan DoS atau DDoS (banjir request yang menghabiskan resource server). Lewat batas, server menjawab 429 Too Many Requests dengan Retry-After (berapa detik menunggu), dan memberi RateLimit-Limit serta RateLimit-Remaining saat masih boleh. Fixed window punya celah di batas jendela, sliding window menghitung 10 detik terakhir dari sekarang, token bucket mengizinkan burst lalu laju rata. Di Laravel: throttle:60,1, dan Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()). Endpoint login dibuat lebih ketat, dan hitungan disimpan di Redis kalau server lebih dari satu.