Taryana
Back to All Technical Articles/Building a Race-Condition-Free Distributed Rate Limiter with Go and Redis
GoRedisDistributed SystemsConcurrencyHigh Performance

Building a Race-Condition-Free Distributed Rate Limiter with Go and Redis

Lessons from real production war stories handling traffic surges and concurrency race conditions: how a sliding-window rate limiter built on atomic Redis Lua scripts in Go reliably handles 200,000+ RPS with zero quota leaks.

Taryana•2026-09-20•9 min read

1. Why Local In-Memory Rate Limiters Inevitably Collapse Across Multi-Pod Clusters

Have you ever been jolted awake in the dead of night by a blaring PagerDuty alert because your primary database CPU suddenly pegged at 100% and backend API servers started toppling like dominoes?

That exact nerve-wracking ordeal is what my engineering team and I lived through when we first launched a high-profile concert ticket flash sale. Tens of thousands of eager users and aggressive scalper bots hammered our endpoints in the exact same second. In our single-node staging environments, our local Go (golang.org/x/time/rate) had looked spotless. We assumed our backend was safely shielded because counters were cleanly tallied in server process RAM.

However, the moment we scaled horizontally across 30 behind a Cloud , our illusion of protection shattered:

  • Massive Global Quota Leakage: If the safety threshold was configured at 10 requests/minute per client IP, a smart bot distributed its connections across 30 separate , successfully executing 300 requests in 60 seconds without ever getting throttled!
  • Complete Isolation: Server A had zero visibility into how many queries Server B or C had already accepted.
  • Database Exhaustion: Thousands of unauthorized queries slipped through the gateway, swamping our primary PostgreSQL database until connection pools collapsed and cascading service outages paralyzed our checkout flow.

From this painful incident, we learned an irreplaceable architectural truth: in distributed systems, global rate quotas must reside in a centralized, ultra-low-latency with guarantees. Our engine of choice was Redis 7, thanks to its execution model, sub-millisecond response times, and native engine.

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

Solution Blueprint: Distributed Sliding-Window To permanently eliminate quota leakage without introducing API gateway latency penalties, we built a 3-pillar distributed architecture:

  1. 1Centralized : Connecting all Go to an Redis cluster with sub-millisecond response times (<1ms) to maintain global quota visibility.
  2. 2 Lua Evaluation: Consolidating the entire evaluation cycle (pruning expired timestamps, counting active requests, and appending the new timestamp) into a single Redis .
  3. 3Zero-Allocation Go Client with Fail-Open Resilience: Utilizing EVALSHA hashes to minimize network bandwidth consumption, paired with a disciplined fail-open policy so transient Redis network partitions never take down primary API availability.

2. Rate Limiting Topology & 5-Algorithm Comparison Matrix

Before committing to an architectural approach, our team evaluated the technical characteristics and trade-offs of five industry-standard algorithms:

AlgorithmQuota PrecisionSpike / Burst AllowanceMemory FootprintEngineering ComplexityProduction Best Use Case
Controlled AccuracySupported (Up to bucket capacity b)Extremely Low (1 Key)ModeratePublic API Gateways (Stripe, GitHub), Mobile & Web APIs
Strict Constant OutflowRejected (Zero Burst Tolerance)Low ( Buffer)Moderate Bank APIs, Third-party Webhook Dispatchers
Fixed WindowVulnerable to Burst ExploitsDangerous (Leaks up to 2x limit)Lowest (1 Integer)LowInternal Background Jobs, Non-critical Daily Quotas
Sliding Log100% Absolute PrecisionStrict ()High (Every Single Timestamp)ModerateSecurity Endpoints, Auth & Defense
Sliding Counter (Lua)100% Absolute PrecisionControlled & FlexibleModerate ( ZSET)Moderate-HighHigh-Throughput Microservices (200k+ ), Core

3. Evaluating 5 Rate Limiting Algorithms in the Trenches

1. Token Bucket: The User-Friendly Standard

Tokens refill continuously into a bucket of fixed capacity at rate r. Each incoming request attempts to claim a token.

  • Strengths: Highly accommodating for human browsing habits, gracefully tolerating legitimate short-lived bursts when users refresh a dashboard page.
  • Drawbacks: Synchronizing token refills across multi-region server clusters requires complex distributed locks or clock synchronization.
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: Downstream Protection Specialist

Incoming requests queue up in a buffer and leak out to downstream services at a strictly metered outflow rate.

  • Strengths: Delivers flawless traffic smoothing; downstream banking partners or legacy databases never experience sudden traffic spikes.
  • Drawbacks: Introduces queue latency for requests entering a saturated buffer.
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: The Boundary Burst Trap

Divides time into static discrete windows (e.g., 10:00:00 - 10:00:59). Every request increments an integer counter.

  • Fatal Vulnerability: An attacker can dispatch 100 requests at second :59 and another 100 requests at second :00 of the subsequent window. Consequently, 200 requests breach the system in just 2 seconds!
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: Flawless Precision at High RAM Cost

Records individual timestamps for every admitted request inside a Redis Sorted Set (ZSET).

  • Strengths: 100% precision with zero boundary leaks.
  • Drawbacks: Consumes excessive RAM under high volumes because every request requires its own timestamp entry in memory.

5. Sliding Window Counter (Redis Atomic Lua): Our Production Choice

Blends the mathematical accuracy of sliding windows with the blazing single-roundtrip performance of an in 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. The Concurrency Race Condition Disaster: Why Naive GET-Then-INCR Always Fails

Problem: The Read-Then-Write Millisecond Window That Admitted Thousands of Bots

The most common mistake engineers make early on is writing logic like this:

Golang Implementation Reference14 baris kode
> // ANTI-PATTERN: Contains a critical concurrency race condition under high load! | func IsAllowedNaive(ctx context.Context, key string, limit int64) (bool, error) { ...Klik untuk melihat implementasi lengkap →

When 5,000 arrive in the exact same millisecond, dozens of server read the identical value of count (e.g., 9) before any has executed INCR. As a result, all 5,000 requests pass the 9 < 10 boundary check, and thousands of illegal queries crash the backend database!

Solution: Unbroken Atomic Execution via Redis Lua Scripting

Redis guarantees that Lua scripts execute transactionally and atomically. No other client command can interleave or modify state while the script runs.

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.

Here is the that purges obsolete timestamps, evaluates active capacity, and registers new requests in an indivisible step:

Redis Lua Script Reference26 baris kode
> -- KEYS[1]: Client identifier bucket key (e.g., "ratelimit:user_123") | -- ARGV[1]: Current timestamp in epoch milliseconds ...Klik untuk melihat implementasi lengkap →

5. High-Throughput Go Implementation with EVALSHA & Fail-Open Resilience

On the Go application side, we encapsulate Lua execution using the go-redis/v9 client library. By initializing the script with redis.NewScript, the client automatically passes a SHA-1 hash (EVALSHA) rather than transmitting raw script text across the network on every request, saving substantial bandwidth:

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

6. Real Production Metrics: Before vs After

We benchmarked this architecture using k6 load testing across our Go cluster confronting peak loads exceeding 200,000 :

Performance MetricBefore (Naive Read-Then-Write)After ( Redis Lua)Production Impact
P99 Latency128 ms (Lock queues & double network hops)1.15 ms (Single roundtrip)99.1% Faster
Quota Rate18.4% (Thousands of unauthorized bot hits)0% (Zero / Zero Leakage)100% Deterministic quota enforcement
Primary Database CPU Load98% (Frequent crashes)Stable under 22%Database remains cool and resilient
Cluster Peak CapacityChoked at 32,000 Sustained 215,000+ 6.7x Throughput expansion

7. 4 Core Engineering Lessons from the Trenches

  1. 1Never Trust Local State Across Clusters: Server memory has zero knowledge of peer nodes. Always rely on a datastore for global rate enforcement.
  2. 2Eliminate Read-Then-Write Gaps with Primitives: Whenever write mutations depend on prior reads, scripting (like Redis Lua) is essential to obliterate race windows.
  3. 3Formulate a Deliberate Fail-Open Strategy: If Redis experiences unexpected connectivity hiccups, temporarily admitting requests (fail-open) is generally far better than returning 500 errors to 100% of your users.
  4. 4Leverage EVALSHA: Always transmit SHA-1 script hashes rather than verbose script payloads to keep internal network bandwidth minimal.

8. Interactive Multi-Algorithm Sandbox Simulator

You can experiment with and directly observe the mechanical differences between these algorithms using the interactive simulator embedded below:

  • Toggle between algorithm tabs (, , , or Fixed Window).
  • Simulate normal steady traffic alongside sudden 15-request burst spikes.
  • On the Fixed Window tab, trigger the exploit button to see how traffic surges across :59s and :00s window transitions leak up to double the configured quota!
Rate Limiter Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →