Implementing API Rate Limiting with Redis: Fixed Window and Sliding Window Approaches in Java
This article explains two API rate‑limiting strategies—fixed‑window using Redis keys and sliding‑window using Redis sorted sets—detailing their principles, drawbacks, and providing complete Java implementations with Lua scripts, AOP annotations, and Redis configuration to enforce request limits.
The article introduces the purpose of API rate limiting: improving system stability and preventing malicious bursts of requests. It poses a typical requirement (e.g., no more than 1000 calls per minute) and outlines two design ideas.
1. Fixed‑time‑window (traditional approach)
1.1 Idea
Each user‑IP and API method is mapped to a Redis key (e.g., IP+method) whose value stores the current request count. The key is created on the first request with an initial value of 1 and an expiration time (e.g., 60 seconds). Subsequent requests increment the value while the key remains unexpired. If the count exceeds the limit, the request is rejected.
1.2 Drawbacks
The fixed window suffers from boundary‑effect problems: a burst can occur at the edge of two windows, effectively allowing up to twice the configured limit within a short interval (e.g., 1999 requests between 00:59 and 01:01 when the limit is 1000 per minute).
2. Sliding‑window (improved approach)
2.1 Idea
Instead of a static window, each request records its timestamp in a Redis sorted set (zSet). When a new request arrives, timestamps older than the configured interval are removed, and the remaining members represent the number of requests in the last time seconds. If this count exceeds the allowed threshold, the request is blocked, guaranteeing a true sliding‑window limit.
2.2 Redis implementation details
Choosing the data structure : a zSet is used because it supports score‑based range queries.
Key design : IP+method as the Redis key; each member is the request timestamp (also used as the score).
Operations :
Add a member: ZADD [key] [score] [member] Remove outdated members: ZREMRANGEBYSCORE [key] 0 [currentTime‑time*1000] Count current members:
ZCARD [key]Code implementation
2.1 Fixed‑window implementation
Rate‑limiting annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
/** limit period in seconds */
int time() default 5;
/** allowed request count */
int count() default 10;
}Lua script (resources/lua/limit.lua)
-- get redis key
local key = KEYS[1]
-- get limit count
local count = tonumber(ARGV[1])
-- get limit time
local time = tonumber(ARGV[2])
-- current value
local current = redis.call('get', key)
if current and tonumber(current) > count then
return tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current)Spring configuration to load the script
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// use Jackson2JsonRedisSerializer for key/value
// ... serializer setup omitted for brevity ...
return redisTemplate;
}
@Bean("limitScript")
public DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}AOP aspect for fixed‑window
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript<Long> limitScript;
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
int time = rateLimiter.time();
int count = rateLimiter.count();
String combineKey = getCombineKey(point);
List<String> keys = Collections.singletonList(combineKey);
Long number = (Long) redisTemplate.execute(limitScript, keys, count, time);
if (number == null || number.intValue() > count) {
throw new RuntimeException("Too many requests, please try later");
}
log.info("[limit] limit='{}' current='{}' key='{}'", count, number.intValue(), combineKey);
}
private String getCombineKey(JoinPoint point) {
StringBuilder sb = new StringBuilder("rate_limit:");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
sb.append(Utils.getIpAddress(request));
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
sb.append("-").append(method.getDeclaringClass().getName())
.append("-").append(method.getName());
return sb.toString();
}
}2.2 Sliding‑window implementation
Rate‑limiting annotation (same as above)
AOP aspect for sliding‑window
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private RedisTemplate redisTemplate;
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
int time = rateLimiter.time();
int count = rateLimiter.count();
String combineKey = getCombineKey(point);
ZSetOperations zSetOps = redisTemplate.opsForZSet();
long now = System.currentTimeMillis();
// add current timestamp as member and score
zSetOps.add(combineKey, now, now);
// set TTL to avoid stale keys
redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);
// remove entries older than the sliding window
zSetOps.removeRangeByScore(combineKey, 0, now - time * 1000);
Long currentCount = zSetOps.zCard(combineKey);
if (currentCount != null && currentCount > count) {
log.error("[limit] limit='{}' current='{}' key='{}'", count, currentCount, combineKey);
throw new RuntimeException("Too many requests, please try later!");
}
}
private String getCombineKey(JoinPoint point) {
StringBuilder sb = new StringBuilder("rate_limit:");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
sb.append(Utils.getIpAddress(request));
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
sb.append("-").append(method.getDeclaringClass().getName())
.append("-").append(method.getName());
return sb.toString();
}
}The article concludes by encouraging readers to share the tutorial, join the community group, and explore additional resources.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
