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.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing API Rate Limiting with Redis: Fixed Window and Sliding Window Approaches in Java

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaopredisspringLua
Java Architect Essentials
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.