DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Optimizing High-Volume REST APIs Using Redis Caching and Spring Boot (With Load Testing Code)
  • Rate Limiting Strategies With Redis: Fixed Window, Sliding Window, and Token Bucket
  • How to Verify Domain Ownership: A Technical Deep Dive

Trending

  • AWS Kiro: The Agentic IDE That Makes Specs the Unit of Work
  • The 7 Pillars of Meeting Design: Transforming Expensive Conversations into Decision Assets
  • Working With Cowork: Don’t Be Confused
  • From APIs to Event-Driven Systems: Modern Java Backend Design
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Building a Rate Limiter and Throttling Layer Using Spring Boot and Redis

Building a Rate Limiter and Throttling Layer Using Spring Boot and Redis

Learn to build a lightweight, in-app rate limiter using Spring Boot and Redis to prevent API overload, brute-force attacks, and accidental misuse.

By 
Vivek Rajyaguru user avatar
Vivek Rajyaguru
·
Sep. 01, 25 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
4.1K Views

Join the DZone community and get the full member experience.

Join For Free

Imagine your backend API is stable, performant and deployed to production. Then someone writes a buggy frontend loop or a bot goes rogue, and suddenly your endpoint gets hit 100 times a second.

That’s how your server’s CPU spikes, your database becomes overloaded, response times shoot up, and eventually your application turns unusable for real users. Even well-architected systems can crumble under this kind of stress, which leads to unhappy customers and costly incidents. 

That's why rate limiting isn't optional but essential for protecting APIs. 

Rate limiting protects your application from brute-force attacks, accidental misuse, or backend overexposure. While tools like API gateways and external services can help, but sometimes you need more control directly inside your application logic. Having it in-app makes debugging easier, behavior more transparent, and rules highly customizable for each endpoint.

In this tutorial, we’ll build a simple and scalable rate limiter using: 

  • Java + Spring boot
  • Redis for distributed storage
  • Spring AOP for annotation-based control

No third-party rate-limiting libraries.  No magic. Just clean, understandable code.

What We’re Building

We'll create a custom annotation-based rate limiter from scratch, complete with:

  • @RateLimit (limit = 5, timeWindowSeconds = 60) annotation
  • Redis-backed logic to store and check request counts
  • A Spring AOP-based layer that intercepts annotated endpoints
  • A custom error handler to send back 429 responses
  • Bonus: Works with distributed servers since Redis is centralized 

Let’s dive into each part.

Define the @RateLimit Annotation

This is the annotation you’ll add to any controller method you want to protect. You can apply different limits to different endpoints without changing the controller logic.

Java
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int limit();
    int timeWindowSeconds();
}


Explanation: 

  • @Target(ElementType.METHOD): You can use it only on methods.
  • @Retention(RetentionPolicy.RUNTIME): It's available during runtime (so AOP can read it).
  • Parameters like limit and timeWindowSeconds make it reusable and flexible

Example usage:

Java
 
@RateLimit(limit = 5, timeWindowSeconds = 60)
    @GetMapping("/api/data")
    public String getData() {
        return "Hello World!";
    }


RedisRateLimiter—Central Logic for Request Tracking

This is the service that talks to Redis and handles the count logic.

Java
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
public class RedisRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean isAllowed(String key, int limit, int timeWindowSeconds) {
        Long currentCount = redisTemplate.opsForValue().increment(key);
        if (currentCount != null && currentCount == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(timeWindowSeconds));
        }
        return currentCount <= limit;
    }

}


Explanation:

  • We generate a unique Redis key for each user + endpoint combination
  • We used INCR (increment) the count each time the endpoint is hit.
  • If it’s the first hit, we also set an expiration (TTL).
  • If the count exceeds the limit, we return false.

Bonus: Redis handles expiration automatically. No cleanup needed.

RateLimitAspect—Interceptor Logic With Spring AOP

This uses Spring AOP to intercept any method with @RateLimit.

Java
 
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private RedisRateLimiter redisRateLimiter;

    @Autowired
    private HttpServletRequest httpServletRequest;

    @Around("@annotation(com.service.ratelimiter.RateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);

        String clientIp = httpServletRequest.getRemoteAddr();
        String redisKey = "ratelimit:"+clientIp+":"+method.getName();

        boolean allowed = redisRateLimiter.isAllowed(redisKey, rateLimit.limit(), rateLimit.timeWindowSeconds());

        if (!allowed) throw new RateLimitExceedException("Too Many Requests. Please try again.");

        return joinPoint.proceed();
    }

}


Why AOP?

Spring AOP allows us to enforce the logic without changing our controller code. In this case It keeps rate limiting:

  • Reusable (Any method, Any controller)
  • Centralized (Logic in one place)
  • Transparent (No business logic impact)

Explanation:

  • It grabs the annotation’s values (limit, timeWindowSeconds).
  • Constructs a Redis key specific to that client and endpoint.
  • Checks if the request should go through or be blocked.

RateLimitExceededException—Custom Exception

Java
 
public class RateLimitExceedException extends RuntimeException {
    public RateLimitExceedException(String message) {
        super(message);
    }
}


Why custom? 

  • Makes it easy to catch and handle just this case.
  • Cleaner than using generic exceptions.

GlobalExceptionHandler—Send Proper 429 Response

Java
 
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;

@ControllerAdvice
public class ExceptionHandler {

    @org.springframework.web.bind.annotation.ExceptionHandler(RateLimitExceedException.class)
    public ResponseEntity<String> handleRateLimitExceeedException(RateLimitExceedException rateLimitExceedException) {
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(rateLimitExceedException.getMessage());
    }
}


Why this matters:

  • 429 Too Many Requests is the proper HTTP code
  • Makes our API more predictable and frontend-friendly
  • Without it, Spring would return a generic 500 error

DemoControllerTry It Yourself

Java
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @RateLimit(limit = 5, timeWindowSeconds = 60)
    @GetMapping("/api/data")
    public String getData() {
        return "Hello World!";
    }

}


Try it: 

  • Hit this endpoint 6 times in a minute using Postman or curl.
  • The 6th request should return:
    { "error": "Too many requests. Please wait and try again." }


Running Redis Locally (Docker)

`docker run --name redis -p 6379:6379 redis`

Add to application.properties:

Plain Text
 
spring.application.name=ratelimiter
server.port=9090
spring.redis.host=localhost
spring.redis.port=6379


Bonus Ideas for Scaling

  • Use user ID or API token instead of IP for multi-user apps.
  • Add metrics (rate_limit_blocked_total) to Prometheus/Grafana.
  • Use Redis Lua scripts for atomic increments + TTL setting
  • Add configurable backoff strategy (e.g., exponential retry)

Why Not Just Use Spring Cloud Gateway or Bucket4j?

Both are great tools, but they add complexity.

For smaller services or internal APIs, a lightweight in-app solution gives:

  • More transparency
  • Fewer dependencies
  • Easier debugging and customization

This approach also helps you understand how rate limiting really works behind the scenes.

Final Thoughts

This rate limiter shows how much power and control you have with just Spring Boot and Redis.

With fewer than 200 lines of code, you can:

  • Protect your services from overload
  • Improve system resilience
  • Scale across nodes with centralized tracking

Beyond the code, the real takeaway is understanding the pattern itself. Once you grasp how rate limiting works internally, you can extend it further by adding features like user-based quotas, adaptive backoff strategies, or real-time monitoring dashboards. These small enhancements can make your system not only more resilient but also smarter in handling unpredictable workloads.

Want the Code?

GitHub Repo: VivekRajyaguru/spring-redis-ratelimiter

rate limit Redis (company) Spring Boot Data Types

Published at DZone with permission of Vivek Rajyaguru. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Optimizing High-Volume REST APIs Using Redis Caching and Spring Boot (With Load Testing Code)
  • Rate Limiting Strategies With Redis: Fixed Window, Sliding Window, and Token Bucket
  • How to Verify Domain Ownership: A Technical Deep Dive

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook