← All articles
Backend1 min read

Rate Limiting Spring APIs with Redis

A shared counter in Redis lets every instance enforce the same request budget. Here's a fixed-window limiter you can drop into a filter.

S

Swapnika Voora

Author

An in-memory counter can't enforce a global limit when requests spread across instances. Redis gives you one authoritative counter that every node increments, so your rate limit means the same thing everywhere.

A fixed-window counter

Increment a per-client key and set its expiry on first use. If the count exceeds the limit within the window, reject the request.

RateLimiter.java
@Component
public class RateLimiter {
 
    private final StringRedisTemplate redis;
 
    public boolean allow(String clientId, int limit, Duration window) {
        String key = "rl:" + clientId;
        Long count = redis.opsForValue().increment(key);
        if (count != null && count == 1L) {
            redis.expire(key, window);
        }
        return count != null && count <= limit;
    }
}

Enforce it in a filter

A servlet filter runs before controllers, so a rejected request never touches your business logic.

RateLimitFilter.java
@Component
public class RateLimitFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest req,
            HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String client = req.getRemoteAddr();
        if (!limiter.allow(client, 100, Duration.ofMinutes(1))) {
            res.setStatus(429);
            return;
        }
        chain.doFilter(req, res);
    }
}

Takeaways

  • A shared Redis counter enforces one global budget across all instances.
  • Set the key's expiry only on the first increment of a window.
  • Reject early in a filter so throttled requests are cheap.

Fixed windows allow bursts at boundaries; for smoother limits, move to a sliding window or token-bucket implemented with a Lua script.

#java#spring-boot#redis#rate-limiting

More in Backend