Lesson 0029 · System Design · Modul Studi kasus · ±20 menit

Studi kasus: rate limiter terdistribusi

Di mana limiter dipasang (gateway atau middleware), counter bersama di Redis, race condition, dan respons 429

Sebelumnya, lesson 0028 menjalankan kerangka 5 langkah untuk sistem read-heavy. Kali ini soalnya berbeda: komponen kecil yang dipanggil di setiap request, sehingga latency tambahan dan kebenaran counter di banyak server menjadi inti diskusinya. Algoritmanya (token bucket, leaky bucket, fixed/sliding window) sudah dibahas di modul Reliabilitas & API; di sini fokusnya pada penerapan terdistribusi.

Target lesson ini: Anda bisa menjawab "Rancang rate limiter untuk API yang berjalan di 10 server" tanpa catatan, dalam 90 detik: posisi limiter, kenapa counter harus bersama (Redis), race condition pada counter dan cara menutupnya, respons 429 + Retry-After, dan trade-off fixed window vs token bucket.

Langkah 1 — Klarifikasi requirement

Langkah 2 — Estimasi

BesaranHitunganHasil
QPS API±5.000 rata-rata × 3±15.000 QPS puncak
Operasi counter1 operasi per request±15.000 operasi/detik di puncak
Memori counter1 juta key × ±100 byte (nama key + angka + TTL)±100 MB
Kalau counter per serverround robin membagi request ke 10 serversetiap server melihat ±1/10, limit efektif ±1.000/menit

Baris terakhir adalah angka yang membenarkan komponen pertama: counter harus bersama untuk semua server. ±100 MB dan ±15.000 operasi/detik adalah beban kecil untuk satu instance Redis (tetap ukur dengan benchmark sebelum memutuskan apa pun). Kalau API hanya berjalan di satu server, counter in-memory atau modul nginx sudah cukup dan Redis tidak perlu.

Langkah 3 — API dan data model

Request ke API apa pun:   GET /v1/orders   X-API-Key: k_123

Lolos  → diteruskan ke handler seperti biasa
Ditolak → HTTP/1.1 429 Too Many Requests
          Retry-After: 17                    ← detik sampai window berikutnya

Redis (counter fixed window per menit)
  key   rl:k_123:202610311405     ← API key + menit saat ini
  value 37                        ← jumlah request di menit ini
  TTL   60 detik                  ← dihapus otomatis setelah window lewat

Aturan (tabel kecil / file config, dibaca berkala)
  paket gratis: 100/menit · paket berbayar: 1.000/menit

Respons penolakan mengikuti RFC 6585: 429 berarti "the user has sent too many requests in a given amount of time ("rate limiting")", dan respons itu "MAY include a Retry-After header indicating how long to wait". [1] Nilai Retry-After boleh berupa jumlah detik atau HTTP-date. [2]

Di mana limiter dipasang?

PosisiKelebihanKekurangan
ClientMengurangi request sia-siaTidak bisa dipercaya: siapa pun bisa memanggil API langsung
API gateway / reverse proxySatu tempat untuk semua service; tersedia siap pakai. AWS API Gateway, misalnya, memakai "the token bucket algorithm, where a token counts for a request" dan client bisa menerima 429 [3]Aturan yang butuh konteks bisnis (paket user) lebih sulit; satu komponen lagi kalau belum punya gateway
Middleware di app serverTahu konteks bisnis (paket, user); tidak menambah hopSetiap service harus memasangnya; counter harus disimpan di luar server

Hati-hati dengan default: modul limit_req nginx memakai metode "leaky bucket" dan secara default menolak dengan status 503, jadi set limit_req_status 429 kalau Anda memakainya. [4]

Latihan 1 — Posisi, algoritma, respons

Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review 0035.

Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0035.

Langkah 4 — High-level design (v1)

Desain v1 paling sederhana yang memenuhi angka: middleware di setiap API server dan satu Redis sebagai counter bersama, dengan fixed window per menit. Redis docs memberi pola ini langsung: INCR + EXPIRE dibungkus MULTI/EXEC "in order to make sure that we'll both increment and set the expire at every API call". [5]

Latihan 2 — Gambar diagram v1 sendiri dulu

Gambar diagram v1 Anda sendiri dulu: dari client sampai handler, di mana counter disimpan, dan apa yang terjadi pada request ke-101. Setelah selesai, buka jawabannya.

Lihat jawaban
Client Load balancer API 1 + limiter API 10 + limiter Redis (counter) INCR + EXPIRE … request ke-101 dalam menit yang sama → 429 + Retry-After
v1: satu Redis karena counter harus bersama untuk 10 server; ±100 MB dan ±15.000 operasi/detik muat di satu node.

Alur: middleware membentuk key rl:{apiKey}:{menit}, menjalankan INCR (+ EXPIRE 60 detik), lalu membandingkan nilai setelah increment dengan batas. Lebih dari 100 → 429; selain itu → handler.

Langkah 5 — Deep dive, bottleneck, trade-off

1. Race condition pada counter. Dua bentuk yang perlu Anda sebut:

-- Lua script (dijalankan atomic di Redis)
local current = redis.call("incr", KEYS[1])
if current == 1 then redis.call("expire", KEYS[1], 60) end
return current

2. Burst di batas window. Fixed window membolehkan 100 request pukul 10:00:59 dan 100 lagi pukul 10:01:00: 200 request dalam dua detik. Kalau requirement melarang burst seperti itu, evolusi ke sliding window counter atau token bucket (keduanya bisa ditulis sebagai Lua script, tetap satu round trip). Kalau requirement "sedikit longgar masih bisa diterima" seperti di langkah 1, fixed window cukup dan lebih murah dipahami.

3. Redis gagal. Requirement bilang API harus tetap jalan, jadi limiter fail-open: pakai timeout pendek ke Redis; kalau gagal, loloskan request dan catat metrik. Untuk availability counter, tambah replica Redis. Trade-off: Redis "uses by default asynchronous replication" [7], jadi saat failover sebagian counter bisa hilang dan beberapa API key sesaat mendapat jatah lebih.

4. Bottleneck berikutnya (belum sekarang). Kalau CPU Redis mendekati batas pada puncak, langkah berikutnya memecah key ke beberapa node (shard berdasarkan API key). Pada ±15.000 operasi/detik ini belum dibenarkan; sebut sinyalnya saja.

Client Load balancer API ×10 + limiter Aturan limit (config) Redis primary (Lua) Redis replica dibaca berkala async timeout pendek, fail-open
v2: Lua script menutup race condition; replica untuk availability counter; aturan per paket dibaca dari config. Sharding Redis baru kalau CPU-nya mendekati batas.

Latihan 3 — Counter dan race condition

Latihan lisan — Ucapkan dengan suara keras

Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Rancang rate limiter: 100 request per menit per API key, untuk API yang berjalan di 10 server." (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"Limiter saya pasang di server, sebagai middleware atau di API gateway, bukan di client karena client tidak bisa dipercaya. Karena ada 10 server di belakang round robin, counter per server membuat limit efektif sekitar 1.000 per menit, jadi counter harus bersama: satu Redis. Estimasinya sekitar 15.000 operasi per detik dan 100 MB untuk 1 juta key, masih muat di satu node. v1-nya fixed window: key berisi API key dan menit, INCR lalu EXPIRE 60 detik, dan saya bandingkan nilai hasil INCR, bukan GET dulu, supaya tidak ada race check-then-act. INCR dan EXPIRE saya gabung dalam Lua script supaya atomic dan key tidak bocor tanpa TTL. Request yang ditolak mendapat 429 dengan Retry-After. Fixed window membolehkan burst dua kali lipat di batas menit; kalau itu tidak boleh, saya ganti sliding window atau token bucket. Kalau Redis down, limiter fail-open dengan timeout pendek."

Checklist: apakah Anda menyebut…

  • posisi di server (middleware atau gateway), bukan client, beserta alasannya?
  • counter bersama di Redis karena counter per server membuat limit efektif 10×?
  • race condition: bandingkan hasil INCR, dan INCR + EXPIRE atomic lewat MULTI atau Lua script?
  • respons 429 dengan Retry-After?
  • trade-off fixed window (burst di batas window) vs sliding window atau token bucket?
  • membenarkan komponen dengan angka (Redis karena 10 server; ±15.000 operasi/detik dan ±100 MB muat di satu node; sharding belum perlu)?

Lulus jika minimal 4 dari 6.

Bacaan lanjutan (opsional, tidak dihitung dalam durasi) Redis docs: INCR, bagian "Pattern: Rate limiter", ±10 menit. Tiga varian rate limiter dan race condition masing-masing.

Berikutnya, lesson 0030: notification system, dengan queue per channel, retry, dan idempotency.

Istilah: GLOSSARY.md (bagian "Studi kasus") · Indeks: semua lesson System Design

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "bagaimana menulis token bucket sebagai Lua script?", "kapan fail-closed lebih tepat dari fail-open?", atau "bagaimana memberi limit berbeda per endpoint?"

Sumber: [1] RFC 6585 bagian 4: 429 Too Many Requests · [2] RFC 9110 bagian 10.2.3: Retry-After · [3] AWS docs: Throttle requests to your REST APIs (API Gateway) · [4] nginx docs: ngx_http_limit_req_module · [5] Redis docs: INCR, Pattern: Rate limiter · [6] Redis docs: Scripting with Lua · [7] Redis docs: Replication