Taryana
Kembali ke Semua Artikel Teknis/Membangun Distributed Rate Limiter Bebas Race-Condition dengan Go dan Redis
GoRedisDistributed SystemsConcurrencyHigh Performance

Membangun Distributed Rate Limiter Bebas Race-Condition dengan Go dan Redis

Belajar dari pengalaman nyata menangani insiden traffic spike dan race condition: bagaimana arsitektur sliding-window rate limiter berbasis atomic Redis Lua script di Go melayani 200.000+ RPS tanpa kebocoran kuota.

1. Mengapa Rate Limiter Lokal Pasti Runtuh di Lingkungan Multi-Pod?

Pernahkah kamu terbangun panik di tengah malam karena ponselmu dibombardir notifikasi alert sistem saat CPU database utama mendadak menyentuh 100% dan server mulai bertumbangan satu per satu?

Pengalaman menegangkan itulah yang pernah saya dan tim hadapi ketika pertama kali menggelar kampanye penjualan kilat (flash sale) tiket konser dengan puluhan ribu pengguna yang menyerbu endpoint API di detik yang sama. Di lingkungan pengujian lokal atau server tunggal, sistem berbasis Go (golang.org/x/time/rate) tampak bekerja sempurna. Kami sempat merasa tenang karena seluruh kuota tampak terhitung rapi di RAM proses server.

Namun, begitu arsitektur di-scale secara horizontal ke kluster 30 di balik , ilusi rasa aman tersebut hancur seketika:

  • Massal (Global Quota Leakage): Jika batas aman ditetapkan 10 request/menit per pengguna, klien bot yang cerdas dapat menembak 30 berbeda dan berhasil mengeksekusi 300 request dalam satu menit tanpa pernah terkena throttle!
  • Isolasi Memori Antar-: Server A tidak memiliki visibilitas terhadap berapa banyak request yang sudah dilayani oleh B atau C.
  • Database Exhaustion: Ribuan request liar yang lolos dari saringan membanjiri PostgreSQL hingga terkuras habis dan situs mengalami .

Dari insiden tersebut, kami menarik satu kesimpulan arsitektural: pada sistem terdistribusi, batas kuota harus disimpan di centralized yang memiliki . Pilihan terbaik kami jatuh pada Redis 7 karena model eksekusi , latensi sub-milidetik, dan dukungannya terhadap script Lua.

Alur Masalah: Kegagalan In-Memory Rate Limiter di Multi-InstanceProblem Flow
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
15 RequestsTraffic SpikeLimit: 10/minIngressRound-RobinGo Pod A (Local Limit)Local Count: 5 / 10✓ Mengira aman (Allow)Go Pod B (Local Limit)Local Count: 5 / 10✓ Mengira aman (Allow)Go Pod C (Local Limit)Local Count: 5 / 10✓ Mengira aman (Allow)DATABASE COLLAPSETotal Masuk: 15 RequestBatas Global: 10 Request💥 OVER-ADMISSION (+50%)
Akar Masalah: Ketika rate limiting hanya disimpan di memori masing-masing server (in-memory isolated), tiap instance Pod tidak mengetahui berapa request yang telah diproses oleh instance lainnya. Akibatnya, batas kuota global bocor sebanyak 150% dan langsung melumpuhkan database utama.
Executive Summary & Core Insight

Blueprint Solusi: Distributed Sliding-Window Untuk membasmi tanpa mengorbankan latensi API gateway, kami merancang arsitektur pembatasan terdistribusi berbasis 3 pilar:

  1. 1Centralized : Menghubungkan seluruh Go ke kluster Redis berlatensi sub-milidetik (<1ms) untuk melacak jejak kuota global.
  2. 2 Lua Evaluation: Memindahkan seluruh siklus validasi (pembersihan window lama, penghitungan kuota aktif, dan pencatatan timestamp baru) ke dalam satu script Lua di Redis.
  3. 3Zero-Allocation Go Client dengan Fail-Open: Mengintegrasikan hash EVALSHA untuk menghemat bandwidth jaringan dan kebijakan fail-open agar gangguan Redis tidak mematikan ketersediaan API utama.

2. Peta Topologi dan Matriks Komparasi 5 Algoritma Rate Limiting

Sebelum menentukan algoritma terbaik, tim kami mengevaluasi karakteristik mekanis dan kompromi dari lima algoritma standar industri:

AlgoritmaPresisi KuotaToleransi Spike / BurstOverhead MemoriKompleksitas TeknisRekomendasi Kasus Penggunaan di Produksi
Cukup AkuratDidukung (Hingga kapasitas b)Sangat Hemat (1 Key)SedangPublic API Gateway (Stripe, GitHub), API Mobile/Web
Aliran Konstan KakuDitolak / Dihaluskan (Zero Burst)Hemat ( Buffer)Sedang Payment API Bank, Dispatcher Webhook Mitra
Fixed WindowRentan Celah BurstBahaya (Bocor hingga 2x kuota)Paling Rendah (1 Integer)RendahBackground Job Internal, Kuota Harian Non-Kritis
Sliding Log100% Presisi TinggiTerkendali ()Boros (Setiap Timestamp)SedangEndpoint Auth, Proteksi Brute Force / Login
Sliding Counter (Lua)100% Presisi TinggiTerkendali & FleksibelTerukur (ZSET ber-)Menengah-TinggiKluster Mikroservis Skala Tinggi (200k+ ), Core

3. Menimbang 5 Algoritma Rate Limiting di Medan Produksi

1. Token Bucket: Sahabat Terbaik Pengguna Manusia

Token ditambahkan secara berkala ke dalam wadah berkapasitas tertentu. Begitu request tiba, sistem mengambil satu token.

  • Kelebihan: Sangat ramah terhadap pengguna riil karena mengizinkan lonjakan singkat () yang wajar, misalnya saat pengguna me-refresh halaman dashboard.
  • Kekurangan: Sinkronisasi pengisian token di kluster multi-region membutuhkan sinkronisasi waktu clock yang presisi.
1. Token Bucket AlgorithmBurst-Friendly
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
Clients InflowNormal 1 req/s+r Refill (+1 token / 1.2s)Token BucketTokens: 8 / 10 (Available)TTTDecision GateTake 1 Token -> 200200 OKForward to Backend429 ThrottledBucket Exhausted🟢 NORMAL MODE: Token terisi konstan. Setiap request mengambil 1 token (HTTP 200 OK).
Traffic Spike: Supported (High Burst)•Memory: Very Low (Single Key)
Fit: API Gateway Publik (seperti Stripe, AWS, GitHub), User-Facing Web/Mobile API di mana user sesekali me-refresh cepat.

2. Leaky Bucket: Penyelamat Integrasi Pihak Ketiga

Request yang masuk ditampung dalam antrean dan dialirkan keluar dengan kecepatan konstan yang teratur.

  • Kelebihan: Menghaluskan trafik (traffic smoothing) secara sempurna; server downstream tidak akan pernah kaget menerima lonjakan mendadak.
  • Kekurangan: Menambah latensi pada request yang mengantre saat trafik padat.
2. Leaky Bucket AlgorithmTraffic Smoothing
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
Clients InflowNormal InflowRequest MasukLeaky Bucket (FIFO)Buffer Antrean (3/10)429 Overflow DroppedBuffer Limit ExceededLaju Bocor Tetap Konstan (Smooth Outflow 50 req/s)Target Backend ServiceTrafik Halus Tanpa Spike!🟢 NORMAL MODE: Request masuk ditampung & dialirkan keluar dengan laju tetap teratur.
Traffic Spike: Rejected (Strict Smooth)•Memory: Low (FIFO Buffer)
Fit: Egress Payment Gateway (ke API Bank / Aggregator kaku), Webhook dispatcher pihak ketiga yang membatasi tepat 50 req/s tanpa toleransi burst.

3. Fixed Window Counter: Jebakan Boundary Burst

Membagi waktu ke dalam blok statis (contoh: 10:00:00 - 10:00:59). Setiap request menambah counter integer.

  • Bahaya Fatal: Klien dapat menembakkan 100 request di detik :59 dan 100 request lagi di detik :00 jendela berikutnya. Akibatnya, 200 request lolos dalam rentang 2 detik saja! Ini berpotensi meruntuhkan database.
3. Fixed Window CounterMemory Minimal
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
Window 1 (10:00:00 - 10:00:59)Batas Kuota: 10 req / menit3 req (Normal):00s Window FlipWindow 2 (10:01:00 - 10:01:59)Batas Kuota: 10 req / menit2 req (Normal)DatabaseLoad Stabil5 reqs/min🟢 NORMAL MODE: Request terdistribusi merata dalam blok 60s. Counter aman di bawah limit.
Traffic Spike: Dangerous (2x Quota Leak)•Memory: Lowest (Atomic Int)
Fit: Internal background jobs, kuota non-kritis per hari (misal 1000 email/hari), di mana lonjakan per detik tidak merusak infrastruktur.

4. Sliding Window Log: Presisi Sempurna dengan Biaya RAM

Menyimpan riwayat timestamp setiap request ke dalam Redis Sorted Set (ZSET).

  • Kelebihan: Nol .
  • Kekurangan: Memakan memori besar jika ada jutaan request aktif per menit karena setiap request memakan 1 elemen ZSET.

5. Sliding Window Counter (Redis Atomic Lua): Pilihan Arsitektur Kami

Menggabungkan keunggulan jendela geser dinamis dengan single-roundtrip di mesin Redis.

4. Sliding Window Counter (Redis Lua)Zero-Race Atomic
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
Expired ZoneZREMRANGEBYSCOREPRUNED> 60s agoActive Window: [Now - 60s s/d Now]Atomic Redis ZSET (Real-time Continuous Log)T:12sT:26sT:41sT:55sNOWLua Atomic Eval1. ZREMRANGE2. ZCARD < 103. ZADD timestampActive: 5/10200 OKDatabase Aman429 ThrottledZero Leakage🟢 NORMAL MODE: Jendela geser kontinu memangkas data usang & catat request aktif via ZSET.
Traffic Spike: Controlled (Optimal)•Memory: Moderate (ZSET TTL)
Fit: Distributed Microservices High-Concurrency (Cloudflare, NGINX, API Core), Flash Sale, Proteksi Transaksi FinTech berskala 200k+ RPS.

4. Tragedi Race Condition di Produksi: Mengapa Pola Naif GET-Lalu-INCR Pasti Gagal?

Masalah: Jeda Waktu Read-Then-Write yang Meloloskan Ribuan Bot

Kesalahan paling umum yang sering dibuat pengembang di awal adalah menulis logika seperti ini:

Golang Implementation Reference14 baris kode
> // ANTI-PATTERN: Mengandung celah race condition fatal di bawah beban tinggi! | func IsAllowedNaive(ctx context.Context, key string, limit int64) (bool, error) { ...Klik untuk melihat implementasi lengkap →

Ketika 5.000 request tiba dalam milidetik yang sama, puluhan server akan membaca nilai count yang sama (misalnya nilai 9) sebelum ada yang sempat memanggil INCR. Akibatnya, seluruh request tersebut menganggap kuota masih di bawah batas (9 < 10) dan ribuan request lolos secara ilegal ke database!

Solusi: Eksekusi Atomik Tanpa Jeda dengan Script Lua

Redis menjamin bahwa script Lua dieksekusi secara dan . Tidak ada perintah klien lain yang dapat menyela di tengah-tengah jalannya script.

Alur Solusi: Atomic Redis Lua Evaluation & Edge ProtectionSolution Flow
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
HTTP Clients10 req/min (Normal)Sliding WindowGo API GatewayEvalSHA Atomic CallShared State Key:ratelimit:user_idRedis 7.2 (ZSET Lua)1. ZREMRANGEBYSCORE2. ZCARD & Evaluate⚡ Sub-milidetik < 0.8msCore DB / BackendHTTP 200 OK✓ Protected from SpikesEdge ThrottledHTTP 429 Too Many🛡️ Zero Database Impact
Solusi Arsitektur: Dengan memindahkan seluruh evaluasi sliding-window ke dalam Redis Atomic Lua Script, kalkulasi kuota terjadi secara atomik dalam single-roundtrip (< 0.8ms). Request berlebih langsung dipotong di edge dengan HTTP 429 tanpa pernah menyentuh atau membebani database utama.

Berikut adalah script Lua yang membersihkan data lama, menghitung kuota aktif, dan mencatat request baru dalam 1 langkah tak terputus:

Redis Lua Script Reference26 baris kode
> -- KEYS[1]: Key identifikasi klien (misal: "ratelimit:user_123") | -- ARGV[1]: Timestamp saat ini dalam milidetik ...Klik untuk melihat implementasi lengkap →

5. Implementasi Golang Skala Tinggi dengan EVALSHA & Fail-Open

Di sisi aplikasi Go, kita membungkus script Lua tersebut menggunakan pustaka go-redis/v9. Dengan memanfaatkan fungsi redis.NewScript, aplikasi secara cerdas mengirimkan hash SHA-1 (EVALSHA) alih-alih mengirimkan teks mentah script di setiap request, menghemat ribuan kilobyte bandwidth jaringan:

Golang Implementation Reference50 baris kode
> // pkg/ratelimit/distributed_limiter.go | package ratelimit ...Klik untuk melihat implementasi lengkap →

6. Hasil Riil di Produksi: Metrik Sebelum vs Sesudah

Pengujian arsitektur dilakukan menggunakan tool k6 pada kluster Go yang berhadapan langsung dengan beban 200.000+ request per detik:

Metrik PengukuranSebelum (Pola Naif Read-Then-Write)Sesudah ( Redis Lua)Dampak Produksi
Latensi P99128 ms (Antrean lock dan double network hop)1,15 ms (Single-roundtrip )99,1% Lebih Cepat
Tingkat 18,4% (Ribuan request ilegal lolos)0% (Nol Kebocoran / Zero )Proteksi kuota 100% deterministik
Beban CPU Database Utama98% (Sering terjadi connection exhaustion)Stabil di bawah 22%Database adem dan bebas crash
Throughput Puncak KlusterTersendat di 32.000 Mencapai 215.000+ Kapasitas melesat lebih dari 6,7x lipat

7. 4 Pembelajaran Penting dari Medan Produksi

  1. 1Jangan Percaya Local Memory State di : Memori lokal server tidak tahu apa yang sedang terjadi di server sebelah. Selalu gunakan untuk proteksi global.
  2. 2Hindari Read-Then-Write Tanpa Lock : Jika keputusan penulisan bergantung pada hasil pembacaan sebelumnya, kamu wajib menggunakan perintah (seperti Redis Lua) untuk melenyapkan jeda .
  3. 3Terapkan Kebijakan Fail-Open secara Bijak: Pada middleware , putuskan kebijakan saat Redis down: lebih baik meloloskan request sementara (fail-open) daripada mematikan seluruh layanan bagi seluruh pelanggan.
  4. 4Optimalkan dengan EVALSHA: Kirimkan hash SHA-1 dari script Lua, bukan teks mentah, agar bandwidth jaringan internal antar-server tetap longgar.

8. Sandbox & Simulator Interaktif Multi-Algoritma

Kamu dapat mencoba dan mengamati langsung perbedaan mekanis setiap algoritma melalui simulator interaktif di artikel ini:

  • Beralih antar tab algoritma (, , , atau Fixed Window).
  • Simulasikan request normal dan tombol lonjakan masif (traffic spike) 15 request sekaligus.
  • Pada tab Fixed Window, tekan tombol simulasi celah untuk menyaksikan langsung bagaimana kuota bisa bocor hingga dua kali lipat di pergantian detik!
Rate Limiter Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →