Rate Limiting & Throttling — System Design

Rate Limiting membatasi jumlah request yang bisa dilakukan dalam periode waktu tertentu. Ini melindungi sistem dari abuse, DDoS, dan overload. Mengapa Rate…

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 /orders maksimal 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();
});

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:

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:

Pasang juga timeout pendek ke Redis (misalnya 50 ms). Limiter yang menunggu Redis lama justru memperlambat semua request.

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari