The Fixed Window Counter is the simplest and most straightforward rate-limiting algorithm. It divides time into fixed intervals (e.g., seconds, minutes, or hours) and counts the number of requests within each interval. If the count exceeds a predefined threshold, the requests are rejected until the next interval begins.
Looking for a more precise algorithm? Take a look at the Sliding Window Log implementation. (Coming soon)
Index
Introduction
How the Fixed Window Counter Rate Limiter Works
Implementation with Redis and Java
Testing with TestContainers and AssertJ
Conclusion (GitHub Repo)
How It Works
.
CODE<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.2.0</version>
</dependency>
Create a FixedWindowRateLimiter class:
The class will take:
A Jedis instance.
A time window size (e.g., 60 seconds).
The maximum number of allowed requests.
CODEpackage io.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.args.ExpiryOption;
public class FixedWindowRateLimiter {
private final Jedis jedis;
private final int windowSize;
private final int limit;
public FixedWindowRateLimiter(Jedis jedis, long windowSize, int limit) {
this.jedis = jedis;
this.limit = limit;
this.windowSize = windowSize;
}
}
Validate the Requests
The main job of this rate limiter is to check if a client is within their allowed request limit. If yes, the request is allowed, and the counter is updated. If not, the request is blocked.
Step 1: Generate a key
We’ll store each client’s request count as a Redis key. To make keys unique for each client, we’ll format them like this:
CODEpublic boolean isAllowed(String clientId) {
String key = "rate_limit:" + clientId;
}
For example, if the client ID is user123, their key would be rate_limit:user123.
Step 2: Fetch the Current Counter
We’ll use Redis’s GET command to check how many requests the client has made so far. If the key doesn’t exist, we assume the client hasn’t made any requests, so the counter is 0.
CODEpublic boolean isAllowed(String clientId) {
String key = "rate_limit:" + clientId;
String currentCountStr = jedis.get(key);
int currentCount = currentCountStr != null ? Integer.parseInt(currentCountStr) : 0;
}
Step 3: Check the Request Limit
Next, we compare the current count to the allowed limit. If the counter is less than the limit, the request is allowed. Otherwise, it’s blocked.
CODEpublic boolean isAllowed(String clientId) {
String key = "rate_limit:" + clientId;
String currentCountStr = jedis.get(key);
int currentCount = currentCountStr != null ? Integer.parseInt(currentCountStr) : 0;
boolean isAllowed = currentCount < limit;
}
Step 4: Increment the Counter and Set Expiration
If the request is allowed**, we need to do two things:
Increment the Counter: Use the Redis INCR command to increase the request count by 1.
Set an Expiration: Use the EXPIRE command to ensure the counter resets at the end of the time window. To make sure the expiration won’t reset everytime we increment the counter, we also need to set the NX flag.
We’ll do this in a transaction to ensure that:
- Both INCR and EXPIRE happen together, avoiding race conditions.
- Both INCR and EXPIRE are pipelined (sent in a batch to Redis) to reduce the number of network trips, improving performance.
CODEif (isAllowed) {
Transaction transaction = jedis.multi();
transaction.incr(key); // Increment the counter
transaction.expire(key, windowSize, ExpiryOption.NX); // Set expiration only if not already set
transaction.exec(); // Execute both commands atomically
}
The first request marks the start of the time window. Any subsequent requests during this window’s lifespan will increment the counter.
Once the window expires, the key is automatically removed from Redis. The next request after that will define the start of a new window.
If we didn’t set the NX flag, the expiration would be reset everytime the counter is incremented, increasing the lifespan of the window.
Complete Implementation
Here’s the full code for the FixedWindowRateLimiter class:
CODEpackage io.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.args.ExpiryOption;
public class FixedWindowRateLimiter {
private final Jedis jedis;
private final int windowSize;
private final int limit;
public FixedWindowRateLimiter(Jedis jedis, long windowSize, int limit) {
this.jedis = jedis;
this.limit = limit;
this.windowSize = windowSize;
}
public boolean isAllowed(String clientId) {
String key = "rate_limit:" + clientId;
String currentCountStr = jedis.get(key);
int currentCount = currentCountStr != null ? Integer.parseInt(currentCountStr) : 0;
boolean isAllowed = currentCount < limit;
if (isAllowed) {
Transaction transaction = jedis.multi();
transaction.incr(key);
transaction.expire(key, windowSize, ExpiryOption.NX); // Set expire only if not set
transaction.exec();
}
return isAllowed;
}
}
And we’re ready to start testing it’s behavior!
Testing our Rate Limiter
To ensure our Fixed Window Rate Limiter behaves as expected, we’ll write tests for various scenarios. For this, we’ll use three tools:
Redis TestContainers: This library spins up an isolated Redis container for testing. This means we don’t need to rely on an external Redis server during our tests. Once the tests are done, the container is stopped, leaving no leftover data.
JUnit 5: Our main testing framework, which helps us define and structure tests with lifecycle methods like @BeforeEach and @AfterEach.
AssertJ: A library that makes assertions readable and expressive, like assertThat(result).isTrue().
Let’s begin by adding the necessary dependencies to our pom.xml.
Adding Dependencies
Here’s what you’ll need in your Maven pom.xml file:
CODE<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.redis</groupId>
<artifactId>testcontainers-redis</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>test</scope>
</dependency>
Once you’ve added these dependencies, you’re ready to start writing your test class.
Setting Up the Test Class
The first step is to create a test class named FixedWindowRateLimiterTest. Inside, we’ll define three main components:
Redis Test Container: This launches a Redis instance in a Docker container.
Jedis Instance: This connects to the Redis container for sending commands.
Rate Limiter: The actual FixedWindowRateLimiter instance we’re testing.
Here’s how the skeleton of our test class looks:
CODEpublic class FixedWindowRateLimiterTest {
private static final RedisContainer redisContainer = new RedisContainer("redis:latest")
.withExposedPorts(6379);
private Jedis jedis;
private FixedWindowRateLimiter rateLimiter;
// Start Redis container once before any tests run
static {
redisContainer.start();
}
}
Preparing the Environment Before Each Test
Before running any test, we need to ensure a clean Redis environment. Here’s what we’ll do:
Connect to Redis: Use a Jedis instance to connect to the Redis container.
Flush Data: Clear any leftover data in Redis to ensure consistent results for each test.
We’ll set this up in a method annotated with @BeforeEach, which runs before every test case.
CODE@BeforeEach
public void setup() {
jedis = new Jedis(redisContainer.getHost(), redisContainer.getFirstMappedPort());
jedis.flushAll();
}
FLUSHALL is an actual Redis command that deletes all the keys of all the existing databases. , , Test)
Stay Curious!
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
Fixed Window Counter Rate Limiter (Redis & Java)
- ↳ Index
- ▸ How It Works
- ↳ 1. Define a Window Interval
- ↳ 2. Track Requests
- ↳ 3. Reset Counter:
- ↳ 4. Rate Limit Check:
- ▸ How to Implement It with Redis and Java
- ↳ 1. Use the INCR command to increment the counter in Redis each time a request is allowed
- ↳ 2. Set the key to expire in one minute if it’s newly created
- ↳ 3. Check the counter for each new request
- ▸ Implementing it with Jedis
- ↳ Start by adding the Jedis library to your Maven file:
- ↳ Create a FixedWindowRateLimiter class:
- ↳ Validate the Requests
- ↳ Complete Implementation
- ▸ Testing our Rate Limiter
- ↳ Adding Dependencies
- ↳ Setting Up the Test Class
- ↳ Preparing the Environment Before Each Test
- ↳ Cleaning Up After Each Test
- ↳ Full Setup
- ↳ Verifying Requests Within the Limit
- ↳ Verifying Requests Beyond the Limit
- ↳ Verifying Requests After Window Reset
- ↳ Verifying Independent Handling of Multiple Clients
- ↳ Verifying Requests Are Denied Until Fixed Window Resets
- ↳ Verifying Denied Requests Are Not Counted
- ↳ GitHub Repo
- ↳ Stay Curious!
SOCIAL SHARE CARD GENERATOR