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.
429 + Retry-After,
dan trade-off fixed window vs token bucket.
Langkah 1 — Klarifikasi requirement
- Functional: batasi setiap API key ke 100 request per menit; request yang melewati batas ditolak dengan pesan jelas; batas bisa berbeda per paket (gratis vs berbayar).
- Skala (asumsi): 1 juta API key aktif per hari; API berjalan di 10 server di belakang load balancer round robin; total ±5.000 QPS rata-rata.
- Non-functional: limiter menambah latency sesedikit mungkin (satu round trip jaringan per request); kalau limiter rusak, API tetap jalan; sedikit lebih longgar dari 100 sesekali masih bisa diterima.
Langkah 2 — Estimasi
| Besaran | Hitungan | Hasil |
|---|---|---|
| QPS API | ±5.000 rata-rata × 3 | ±15.000 QPS puncak |
| Operasi counter | 1 operasi per request | ±15.000 operasi/detik di puncak |
| Memori counter | 1 juta key × ±100 byte (nama key + angka + TTL) | ±100 MB |
| Kalau counter per server | round robin membagi request ke 10 server | setiap 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?
| Posisi | Kelebihan | Kekurangan |
|---|---|---|
| Client | Mengurangi request sia-sia | Tidak bisa dipercaya: siapa pun bisa memanggil API langsung |
| API gateway / reverse proxy | Satu 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 server | Tahu konteks bisnis (paket, user); tidak menambah hop | Setiap 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
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:
- Check-then-act:
v = GET(key); if v < 100 then INCR(key). Dua server membaca 99 pada saat yang sama, keduanya lolos, counter menjadi 101. Perbaikan: jangan membaca dulu.INCRadalah operasi atomic yang mengembalikan nilai baru, jadi bandingkan hasilINCRitu dengan batas. - Key bocor: Redis docs mencatat varian single counter (
INCR, laluEXPIREkalau nilainya 1) punya race condition: "If for some reason the client performs the INCR command but does not perform the EXPIRE the key will be leaked". Perbaikannya: jadikan keduanya satu Lua script yang dikirim denganEVAL. [5] Redis "guarantees the script's atomic execution", jadi tidak ada server lain yang menyisip di tengah. [6]
-- 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.
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, danINCR+EXPIREatomic lewatMULTIatau Lua script? - respons
429denganRetry-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.
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