How Bella OpenAPI Implements Sliding Window Rate Limiting with Redis Lua Scripts
Bella OpenAPI enforces per-minute request quotas using an atomic Lua script that maintains a rolling 60-second window in Redis, eliminating race conditions through single-call execution while storing request timestamps in a Sorted Set.
Bella OpenAPI (lianjiatech/bella-openapi) handles high-throughput API traffic by implementing a precise sliding-window rate limiter written in Lua and executed directly inside Redis. This approach tracks every request timestamp atomically, ensuring strict RPM (requests per minute) enforcement without the burst vulnerabilities of fixed-window counters.
The Architecture of the Sliding Window Limiter
The rate-limiting flow begins at the API gateway layer and terminates in a single atomic Redis operation. When a request arrives, the RateLimitInterceptor (located in com.ke.bella.openapi.intercept) extracts the API key, target channel, and a unique requestId.
The RateLimiterService then constructs two distinct Redis keys for each client:
- ZSET key:
rpm:{apiKey}stores the actual request timestamps as members (usingrequestIdfor uniqueness) - String key:
rpm:cnt:{apiKey}holds the current minute's running count
These keys are passed to the Lua script along with the current timestamp and request ID.
Inside the rpm.lua Script
The core logic resides in api/server/src/main/resources/lua/limiter/rpm.lua. This script runs atomically inside Redis through Spring Data Redis's DefaultRedisScript mechanism.
The script executes the following operations in a single pcall block:
- Calculate window boundary: Computes the sliding start time as
now - 60seconds - Expire old entries: Uses
ZREMRANGEBYSCOREto purge timestamps older than the window - Record new request: Adds the current
requestIdto the ZSET usingZADD - Update counter: Adjusts the count key via
INCRBYto reflect net changes - Set TTLs: Configures the ZSET to expire after 2 minutes and the count key after 1 minute
- Return status: Returns
"OK"on success or captures errors for Java-side handling
Java Integration and Script Execution
The Java layer loads and executes the script through StringRedisTemplate. In RateLimiterService, the system initializes a DefaultRedisScript<String> pointing to the classpath resource lua/limiter/rpm.lua.
@Service
public class RateLimiterService {
private final StringRedisTemplate redisTemplate;
private final DefaultRedisScript<String> rpmScript;
public RateLimiterService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.rpmScript = new DefaultRedisScript<>();
this.rpmScript.setScriptSource(
new ResourceScriptSource(
new ClassPathResource("lua/limiter/rpm.lua")));
this.rpmScript.setResultType(String.class);
}
public void checkRateLimit(String apiKey, String requestId) {
String key = "rpm:" + apiKey;
String countKey = "rpm:cnt:" + apiKey;
long now = System.currentTimeMillis() / 1000;
List<String> keys = Arrays.asList(key, countKey);
List<String> args = Arrays.asList(String.valueOf(now), requestId);
String result = redisTemplate.execute(rpmScript, keys, args.toArray());
if (!"OK".equals(result)) {
throw new RateLimitExceededException("RPM limit exceeded");
}
}
}
If the script returns anything other than "OK", the interceptor throws an exception resulting in an HTTP 429 Too Many Requests response.
Design Advantages of the Sliding Window Approach
Unlike fixed-window counters that reset at minute boundaries and allow traffic spikes immediately after reset, the sliding window algorithm counts only requests that fall within the last 60 seconds. This smooths traffic patterns while guaranteeing the configured RPM limit is never exceeded.
You can test the script directly via redis-cli:
redis-cli --eval api/server/src/main/resources/lua/limiter/rpm.lua rpm:test rpm:cnt:test , $(date +%s) req-1
# Returns: OK
Key Reliability Features
The implementation provides four critical guarantees through its Lua architecture:
- Atomicity: All Redis commands wrap in a
pcallblock, ensuring either complete success or zero state change - Idempotency: The
requestIdparameter prevents duplicate retries (network hiccups) from inflating the count, asZADDinserts each member only once - TTL Management: Memory efficiency comes from aggressive expiration—the ZSET lives 2 minutes (covering the window plus safety margin) while the plain count expires in exactly 1 minute
- Error Handling: Lua errors return as string arrays that the Java layer converts to clear exceptions rather than silent failures
Summary
- Bella OpenAPI implements sliding-window RPM limiting through a Lua script at
api/server/src/main/resources/lua/limiter/rpm.lua - The script uses Redis Sorted Sets (
ZSET) to track request timestamps and a String key to maintain the current count - Execution happens atomically via
StringRedisTemplate.execute(), preventing race conditions during concurrent access - The sliding window calculates
now - 60sboundaries dynamically, avoiding the burst vulnerabilities of fixed-window algorithms - Design features include idempotent request tracking via unique IDs and automated TTL cleanup to minimize memory footprint
Frequently Asked Questions
What is the difference between the ZSET key and the count key in Bella OpenAPI's rate limiter?
The ZSET key (rpm:{apiKey}) stores individual request timestamps as members with the requestId as the member value, enabling the sliding window to remove expired entries precisely. The count key (rpm:cnt:{apiKey}) is a simple String that holds the current integer count of requests in the active window, updated atomically via INCRBY within the Lua script.
How does the Lua script prevent race conditions during high concurrency?
All operations—removing old entries, adding new timestamps, and updating the counter—execute inside a single pcall block within Redis. Because Redis executes Lua scripts atomically (blocking other commands during execution), no other client can modify the keys between the ZREMRANGEBYSCORE and ZADD operations, eliminating read-modify-write race conditions.
Why use a sliding window instead of a fixed window for RPM limiting?
Fixed-window counters reset at arbitrary minute boundaries (e.g., XX:00, XX:01), allowing users to make requests at XX:01:59 and again at XX:02:00, effectively doubling their limit within two seconds. The sliding window algorithm calculates the window as a rolling 60-second period (current_timestamp - 60), ensuring the request count always reflects exactly the last minute of activity regardless of when the minute started.
What happens when the Redis Lua script returns an error?
Any error during script execution—whether from Redis command failures or Lua runtime issues—is caught by the pcall wrapper and returned as a string array to the Java caller. The RateLimiterService checks the return value, and if it is not the literal string "OK", it throws a RateLimitExceededException or other appropriate runtime exception, causing the RateLimitInterceptor to reject the request with a 429 status code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →