Rate limiting membatasi berapa request yang boleh dikirim satu client dalam rentang waktu tertentu. Di lesson ini fokusnya desain: limiter yang dipakai bareng oleh banyak server. Bayangin venue konser dengan 24 pintu masuk, dan aturannya tiap orang maksimal masuk 10 kali. Kalau tiap penjaga pintu menghitung sendiri di buku catatannya, orang yang pindah-pindah pintu bisa masuk 24 × 10 = 240 kali, dan tiap penjaga merasa aturannya dijalankan. Supaya aturannya benar, semua penjaga harus bertanya ke satu meja pusat lewat HT sebelum membuka pintu.
🧪 Tebakan cepat: Saat war tiket, TiketNada menjalankan 24 server app. Tiap server membatasi
POST /ordersmaksimal 10 request per 10 detik per akun, dihitung di memorinya sendiri. Bot calo menembak 300 request dalam 10 detik, dan load balancer membaginya rata. Berapa yang lolos? Tebak dulu, jawabannya ada di bagian "Satu limit, 24 server".
Satu limit, 24 server
Dasar rate limiting (kenapa perlu, jawaban 429, tiga cara menghitung) sudah ada di lesson Rate Limiting track API, dan versi untuk login ada di Rate Limiting untuk Autentikasi. Contoh di sana jalan di satu server. TiketNada di jam 10.00 bukan satu server: dari lesson Apa Itu System Design, war tiket butuh 24 server app di belakang load balancer.
Aturannya: akun calo99 boleh POST /orders maksimal 10 kali per 10 detik. Bot-nya mengirim 300 request dalam 10 detik, 30 per detik, dan load balancer membaginya bergiliran (round robin). Kalau counter-nya disimpan di memori tiap server, tiap server cuma melihat bagiannya sendiri:
| Server app | Request yang diterima tiap server | Lolos (maks 10 per server) | Total lolos |
|---|---|---|---|
| 1 | 300 | 10 | 10 |
| 2 | 150 | 10 | 20 |
| 6 | 50 | 10 | 60 |
| 24 | 12 atau 13 | 10 | 240 |
Jawaban tebakannya: 240 lolos, 24 kali batasnya. Batas efektifnya jadi jumlah server × limit, dan makin banyak server ditambah untuk war tiket, makin longgar limiter-nya. Tiap server benar menurut catatannya sendiri; yang salah adalah tempat menyimpan counter-nya.
Satu counter bersama di Redis
Obatnya: semua server bertanya ke satu tempat. Redis cocok: datanya di memori, satu perintah butuh sekitar 0,5 ms, dan perintahnya atomic (dijalankan satu per satu, gak bisa disela). Ini limiter fixed window (satu counter per jendela 10 detik) dengan ioredis:
const LIMIT = 10;
const JENDELA = 10; // detik
async function bolehLewat(userId) {
const nomorJendela = Math.floor(Date.now() / 1000 / JENDELA);
const key = `rl:${userId}:${nomorJendela}`;
const [[, jumlah]] = await redis.multi().incr(key).expire(key, JENDELA).exec();
return jumlah <= LIMIT;
}
app.post("/orders", async (req, res, next) => {
if (!(await bolehLewat(req.user.id))) {
const sisaDetik = JENDELA - (Math.floor(Date.now() / 1000) % JENDELA);
res.set("Retry-After", String(sisaDetik));
return res.status(429).json({ error: "Terlalu banyak request" });
}
next();
});
nomorJendelaadalah nomor jendela 10 detik saat ini. Jam 10.00.00 WIB tanggal 9 Oktober 2026 itu jendela179151480, jadi key-nyarl:calo99:179151480. Jendela berikutnya otomatis pakai key baru.INCRmenambah counter dan mengembalikan nilai barunya dalam satu langkah atomic. 24 server yang memanggilnya bersamaan tetap mendapat 1, 2, 3, ... tanpa ada yang terlewat.EXPIREmembuat key-nya hilang sendiri setelah 10 detik, supaya Redis gak penuh key lama.multi()...exec()mengirim keduanya sebagai satu transaksi Redis (MULTI/EXEC): dijalankan berurutan tanpa disela perintah client lain, dalam satu kali bolak-balik ke Redis. Alternatifnya Lua script, yang juga atomic.- Lewat batas:
429 Too Many RequestsplusRetry-After, berapa detik lagi boleh mencoba.
Di redis-cli, request pertama bot di jendela ini terlihat begini:
> MULTI
OK
> INCR rl:calo99:179151480
QUEUED
> EXPIRE rl:calo99:179151480 10
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
Request ke-11 mendapat 1) (integer) 11, jadi ditolak. Dengan counter bersama, berapa pun jumlah server-nya, yang lolos tetap 10.
GET lalu SET: bersama tapi tetap bocor
Counter di Redis belum cukup kalau cara menaikkannya salah. Versi yang sering ditulis pemula:
const jumlah = Number(await redis.get(key)) || 0; // baca
if (jumlah >= LIMIT) return false;
await redis.set(key, jumlah + 1, "EX", JENDELA); // tulis
return true;
Antara GET dan SET ada celah. Server yang menerima request di milidetik yang sama membaca nilai yang sama, misalnya 4, lalu sama-sama menulis 5. Lima request lolos, counter cuma naik satu. Di model Lab, dengan 24 server, 150 dari 300 lolos: lebih baik dari 240, tapi tetap 15 kali batasnya. Ini race condition: hasilnya bergantung pada siapa duluan. INCR menutup celahnya, karena baca, tambah, dan tulis terjadi sebagai satu perintah di dalam Redis.
Tiga algoritma dan memorinya
Tiga algoritma dari lesson Rate Limiting track API, dilihat dari sisi desain: berapa memori yang dipakai per pengguna.
| Algoritma | Yang disimpan per pengguna | Byte mentah | Perilaku |
|---|---|---|---|
| Fixed window | 1 counter | 8 | sederhana; di batas jendela bisa lolos 2 × limit dalam waktu singkat |
| Token bucket | jumlah token + waktu isi ulang | 16 | boleh burst sampai kapasitas bucket, lalu ikut kecepatan isi ulang |
| Sliding window log | waktu setiap request di jendela | limit × 8 | paling akurat, memori tumbuh sesuai limit |
Token bucket berisi token sebanyak kapasitasnya, misalnya 10. Tiap request mengambil 1 token, dan token terisi lagi dengan kecepatan tetap, misalnya 1 per detik. Bot yang diam lama lalu menembak 15 request sekaligus cuma lolos 10: burst boleh, tapi gak lebih dari kapasitas. Fixed window punya batas keras per jendela yang di-reset berkala, jadi 10 request di detik 9,9 dan 10 lagi di detik 10,1 lolos semua.
Untuk 1.000.000 pengguna aktif (DAU TiketNada), datanya saja:
- Fixed window: 1.000.000 × 8 byte = 8 MB.
- Token bucket: 1.000.000 × 16 byte = 16 MB.
- Sliding window log dengan limit 10: 1.000.000 × 10 × 8 byte = 80 MB. Dengan limit 1.000 per jam: 8 GB.
Redis menambah puluhan byte overhead per key, jadi angka sebenarnya lebih besar, tapi perbandingannya sama: log memakai memori sebanyak limit-nya. Jalan tengahnya sliding window counter: dua counter fixed window (jendela ini dan sebelumnya), dan jumlah request diperkirakan dari bobot keduanya. Hampir seakurat log, cuma 16 byte. Di Redis, fixed window cukup INCR; token bucket dan sliding window log butuh Lua script atau MULTI supaya baca-hitung-tulisnya atomic.
🤔 Kalau Redis-nya mati
Limiter bersama menambah satu ketergantungan. Kalau Redis gak menjawab, pilih sejak awal:
- Fail open: loloskan semua request. Aplikasi tetap jalan, limiter-nya hilang sementara. Cocok untuk batas yang sifatnya keadilan, misalnya
GET /events. - Fail closed: tolak semua dengan
429atau503. Aman, tapi aplikasi ikut mati. Cocok untuk endpoint mahal atau rawan, misalnya login. - Jalan tengah: kembali ke limiter lokal per server dengan limit dibagi jumlah server (10 ÷ 24, dibulatkan ke atas jadi 1 per server). Kasar, tapi masih membatasi.
Pasang juga timeout pendek ke Redis (misalnya 50 ms). Limiter yang menunggu Redis lama justru memperlambat semua request.
⚠️ Jebakan yang sering ketemu
- Counter di memori tiap server. Batas efektifnya jadi jumlah server × limit, dan makin longgar saat server ditambah.
- GET lalu SET. Counter-nya bersama tapi gak atomic; request bersamaan lolos semua. Pakai
INCR, MULTI/EXEC, atau Lua script. INCRdanEXPIREdi dua perintah terpisah, tanpa MULTI. Kalau server crash di antaranya, key tanpa nomor jendela gak pernah kedaluwarsa, dan akunnya terblokir selamanya.- Sliding window log untuk limit besar. Limit 1.000 per jam untuk sejuta pengguna berarti gigabyte. Pakai token bucket atau sliding window counter.
- Lupa
Retry-After. Client yang gak tahu kapan boleh mencoba lagi akan terus memukul server.
TL;DR: Rate limit yang dihitung di memori tiap server bocor saat server-nya banyak: di 24 server, bot yang dibatasi 10 per 10 detik lolos 240 kali, karena batas efektifnya jumlah server × limit. Solusinya satu counter bersama di Redis yang dinaikkan secara atomic: INCR dan EXPIRE dalam satu MULTI/EXEC (atau Lua script), dengan nomor jendela di key-nya (rl:calo99:179151480). Membaca lalu menulis (GET lalu SET) tetap bocor karena race condition (150 lolos di model Lab). Lewat batas, jawab 429 Too Many Requests dengan Retry-After. Token bucket membolehkan burst sampai kapasitas bucket lalu mengikuti kecepatan isi ulang; fixed window punya batas keras per jendela yang di-reset berkala. Memorinya per pengguna: fixed window 8 byte, token bucket 16 byte, sliding window log limit × 8 byte (sejuta pengguna: 8 MB, 16 MB, 80 MB). Tentukan juga perilakunya saat Redis mati: fail open atau fail closed.