# How Bella OpenAPI Implements Sliding Window Rate Limiting with Redis Lua Scripts

> Discover how Bella OpenAPI leverages Redis Lua scripts for atomic sliding window rate limiting. Learn how it prevents race conditions and enforces request quotas effectively.

- Repository: [Ke Technologies/bella-openapi](https://github.com/lianjiatech/bella-openapi)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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 (using `requestId` for 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`](https://github.com/lianjiatech/bella-openapi/blob/main/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:

1. **Calculate window boundary**: Computes the sliding start time as `now - 60` seconds
2. **Expire old entries**: Uses `ZREMRANGEBYSCORE` to purge timestamps older than the window
3. **Record new request**: Adds the current `requestId` to the ZSET using `ZADD`
4. **Update counter**: Adjusts the count key via `INCRBY` to reflect net changes
5. **Set TTLs**: Configures the ZSET to expire after 2 minutes and the count key after 1 minute
6. **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`](https://github.com/lianjiatech/bella-openapi/blob/main/lua/limiter/rpm.lua).

```java
@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`:

```bash
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 `pcall` block, ensuring either complete success or zero state change
- **Idempotency**: The `requestId` parameter prevents duplicate retries (network hiccups) from inflating the count, as `ZADD` inserts 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`](https://github.com/lianjiatech/bella-openapi/blob/main/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 - 60s` boundaries 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.