Building a Distributed Rate Limiter
Thread-safe Python reference implementations:: ON THIS PAGE 4
Overview
First, let's define what a rate limiter is. It's a system that controls the rate at which requests are processed. Ok, simple enough. But how does this work in a distributed system? Each service cannot have its own logic for rate limiting as it would cause issues with consistency.
So that means we need a single source of truth for rate limiting. For this example we will use Redis as our rate limiter. More specifically we will create a python implementation that interfaces with Redis using INCR and EXPIRE commands to implement a token bucket.
Token Bucket
Token bucket is a rate limiting algorithm that allows a fixed number of requests per second. It works by maintaining a bucket of tokens, and each request consumes a token. If the bucket is empty, the request is rejected.
$$ \text{tokens}(t) = \min\left(\text{capacity}, \text{tokens}(t_0) + \text{rate} \times (t - t_0)\right) $$
Where:
- $\text{tokens}(t)$ = available tokens at time $t$
- $\text{capacity}$ = maximum bucket size
- $\text{rate}$ = token refill rate (tokens per second)
- $t_0$ = last refill time
A request is allowed if:
$$ \text{tokens}(t) \geq \text{cost} $$
Simple Implementation
=
=
=
=
=
# 1. Refill tokens based on time passed
= -
+= *
# 2. Cap at capacity
=
=
# 3. Check if we have enough
-=
return True
return False
The Problem
This fails immediately in a multi-threaded environment (like Flask or Django). Two threads could read self.tokens at the exact same time (e.g., value 5), both subtract 1, and write back 4. But it should be 3. This is a Race Condition.
Thread Safety
In order to deal with this race condition we have to make sure that the allow_request method is thread-safe. In Python this can be achieved using a Lock (Mutex).
=
=
=
=
= # Mutex
# Acquire the lock
=
= -
+= *
=
=
-=
return True
return False
Excellent! Now we have a thread-safe implementation, this will work great... on one server.
Scenario:
User Amakes a request to Server 1, they get a tokenUser Amakes a request to Server 2. Server 2 has no idea thatUser Aalready got a token from Server 1. All it knows is that this user as far as its concerned has 1 token left. So it allows the request.
User A effectively now has Limit * Number of Servers tokens.
This is where we need a distributed rate limiter.
Distributed State
=
=
=
=
= f
"""
Lua script to atomicaly:
1. Refill tokens based on time passed.
2. Check if enough tokens exist.
3. Decrement and update timestamp.
"""
=
=
return
Notice how we are not using Lock. This is because the Lua script is atomic. It will execute in a single step and no other thread can access the state in between.
What's Next
The token bucket is just one approach. Other rate limiting algorithms worth exploring:
- Sliding Window Log — tracks exact timestamps of each request, more precise but uses more memory
- Sliding Window Counter — hybrid of fixed window and sliding log, good balance of accuracy and efficiency
- Leaky Bucket — smooths out burst traffic by processing requests at a fixed rate