Spring Boot Rate Limiting Example based on Redis

By Ercan - 15/10/2025 - 0 comments

Rate limiting is one of the most fundamental building blocks in backend engineering — it protects your system from excessive requests, ensures fair usage, and prevents overload.
While there are countless ways to implement it, achieving both accuracy and efficiency in a distributed environment is a more delicate challenge.

In this post, I’ll share my hands-on experience building a Redis-based rate limiting system in Spring Boot, comparing five popular algorithms, and highlighting how Lua scripting can significantly improve performance and reliability.


Project Overview

The project, available on GitHub (ercansormaz/redis-rate-limiting), demonstrates how different rate limiting algorithms behave under various load conditions.

It implements the following algorithms:

  • Token Bucket
  • Leaky Bucket
  • Fixed Window
  • Sliding Window Counter (Weighted)
  • Sliding Window Log

Each algorithm can be tested through its dedicated endpoint, for example:

GET /rate-limiter/token-bucket
GET /rate-limiter/leaky-bucket
GET /rate-limiter/fixed-window
GET /rate-limiter/sliding-window-counter
GET /rate-limiter/sliding-window-log

If the request is within the limit, the API responds with 202 Accepted.
If the request exceeds the limit, it returns 429 Too Many Requests — no response body is sent.
This minimalistic design makes it easy to observe limiter behavior under testing and load.


From Java Logic to Lua Scripts

In the first iteration, all Redis operations were handled directly through Java using RedisTemplate.
While functional, this approach wasn’t atomic — multiple commands meant potential race conditions when multiple instances handled concurrent requests.

To address this, each algorithm was refactored to use Lua scripts executed directly inside Redis.
This ensures atomicity and reduces network overhead, as the logic is processed server-side.

All previous Java-based operations were intentionally left in the codebase as commented examples to help readers understand the transition between pure Java and Lua-based implementations.


Why Lua Scripts Matter

Executing rate limiting logic through Lua offers several key benefits:

  • Atomic operations: the entire logic (increment, expire, cleanup) executes in a single Redis context.
  • Lower latency: fewer network round-trips between application and Redis.
  • Consistency: no race conditions even under high concurrency.

But there’s another layer to this optimization that often goes unnoticed — how Lua scripts are executed.


Script Execution Optimization: EVAL vs EVALSHA

When using RedisTemplate, scripts are executed via the EVAL command.
This means the entire Lua script is sent to Redis on every invocation, which can become costly under heavy load.

To avoid this, my implementation uses RedisCommands directly and executes scripts through EVALSHA instead.
Here’s why this matters:

  • EVALSHA references the script by its SHA1 hash, allowing Redis to reuse the cached script instead of receiving it every time.
  • If the script isn’t yet loaded (Redis returns NOSCRIPT), the code automatically falls back to a one-time EVAL to load it — then resumes using EVALSHA.

This approach combines atomic execution with maximum efficiency, reducing both bandwidth and CPU overhead on Redis.


Design Details

Annotation-Driven Architecture

Each algorithm is exposed through a dedicated annotation such as:

@TokenBucketLimit(...)
@LeakyBucketLimit(...)
@FixedWindowLimit(...)
@SlidingWindowCounterLimit(...)
@SlidingWindowLogLimit(...)


These annotations are intercepted using Spring AOP, cleanly separating rate limiting from business logic.


Weighted Sliding Window Counter

The Sliding Window Counter algorithm in this project uses the weighted variant instead of the classic approach.
Traditional counters assign equal weight to all sub-windows, which can cause abrupt spikes at window boundaries.

In the weighted model, counts are proportionally distributed based on elapsed time within each sub-window.
This provides smoother rate control and a more natural decay of allowed requests.


Sliding Window Log Optimization

Most reference implementations log all requests, even those exceeding the rate limit.
However, this can flood Redis with timestamps and permanently saturate the sliding window during high traffic bursts.

In this project, rejected requests are not logged.
This prevents continuous blocking and ensures that once older timestamps expire, new requests can pass — maintaining stable throughput under load.


Example Lua Script Execution

Here’s a simplified Lua snippet used to handle request counting atomically:

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local requestId = ARGV[4]

-- Remove expired entries
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- Count current requests
local count = redis.call('ZCOUNT', key, now - window + 1, now)

if count < limit then
    redis.call('ZADD', key, now, requestId)
    redis.call('PEXPIRE', key, window)
    return 1
end

return 0


In Java, this is executed via evalsha through the RedisCommands interface, ensuring that Redis handles the logic atomically and efficiently.


Observing the Behavior

Testing is straightforward:

  1. Start Redis (via Docker, Podman or locally).
  2. Run the Spring Boot application.
  3. Send repeated requests to one of the /rate-limiter/* endpoints.

You’ll quickly notice how each algorithm behaves differently under the same rate limit configuration — especially the smoothness of the weighted sliding window and the burst tolerance of the token bucket.


Lessons Learned

  • Atomicity matters: without Lua, Redis operations can easily race under high concurrency.
  • Evalsha is a must: caching Lua scripts in Redis drastically improves efficiency.
  • Weighted sliding windows rock: they provide more stable rate control than simple counters.
  • Don’t log rejected requests: it keeps your limiter responsive even during bursts.

Full Source Code

The complete implementation — including all five algorithms, annotations, and Lua scripts — is available on GitHub:
👉 github.com/ercansormaz/redis-rate-limiting


Conclusion

Building a robust rate limiter isn’t just about counting requests — it’s about balancing accuracy, efficiency, and scalability.
Redis, combined with Lua scripts and a well-designed architecture, provides an elegant solution to this classic backend challenge.

If you’re looking for a production-grade yet educational example, feel free to explore the repository, experiment with different algorithms, and benchmark the results.

Tags: spring boot, rate-limiting, redis