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-timeEVAL
to load it — then resumes usingEVALSHA
.
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:
- Start Redis (via Docker, Podman or locally).
- Run the Spring Boot application.
- 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