ideas

Java Spring MVC rate limit

We are developing a simple mobile app which has a rest api. Team consists of a backend developer(Me me me!), ionic developer, designer and web developer. Since I don't like complexity in my code, I always try to isolate my work with other team members technically, so developing Rest API is a great way both for me and front end developers to work at the same time.

In this project we've decided to block multiple user requests and block requests after rate limit exceeded. Requirement is so simple, in 15 minutes each user can request maximum 150 GET requests and 15 POST requests. System works scaled behind a load balancer, so request count shall be calculated for system not for each machine. So I decided to use AWS Elasticache for each machine to update rate limit value. For filtering each requests, I implemented Spring's interceptor. In this interceptor I checked the user's identity and update it's rate limit usage value in cache. If the value is greater than the threshold, I blocked user's request and return HTTP code 429.

Here is the work flow of the implementation:

  • Spring interceptor integration includes config update and interceptor class
  • Connecting to Elasticache and updating rate limits
  • Continue to process request or block request

It's so simple right? Lets go step by step.

1- Interceptor class

Spring Interceptors has the ability to pre-handle and post-handle the web requests and interceptor classes should extend the HandlerInterceptorAdapter. By using interceptor class you can filter all requests to count for rate limit operations. Here is a simple interceptor class you can use.

package in.soner.web.interceptor;

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

@Service
public class RateLimitInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RateLimitService rateLimitService;

    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler)
            throws Exception {
        // this is where magic happens
        return true;
    }

}
2- Enable the handler

Put your handler interceptor class in the handler mapping "interceptors" property. In this example I use mapping path ** to intercept all requests.

 <mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="in.soner.web.interceptor.RateLimitInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>  
3- Rate Limit Service

In my example we want to give 150 GET and 15 POST request per each user. So I need to know how much seconds remained for next quarter minute. For this I wrote simple function updateExpireTimeForCache and it refresh timeToNextQuarter value in each 10 seconds. I use timeToNextQuarter for cache expiration, so automatically cache removes userKey in each 15 minutes. In my case, I use Memcached for POC but you can easily use Redis as well.

@Service
public class RateLimitServiceImpl extends BaseApplicationContext implements RateLimitService {

    public static int timeToNextQuarter = 15;

    private Calendar calendar = Calendar.getInstance();

    @Scheduled(cron = "0/10 * * * * *")
    public void updateExpireTimeForCache() {
        int currentMinute = calendar.get(Calendar.MINUTE);
        int mod = currentMinute % 15;
        calendar.set(Calendar.MINUTE, currentMinute - mod + 15);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        timeToNextQuarter = (int) ((calendar.getTime().getTime() - new Date().getTime())/1000);
    }

    @Override
    public long incrementLimit(String userKey) {
        return cache.incr(userKey, 1, 1, timeToNextQuarter);
    }
}

So everything is ready! Memcached returns updated value when incr called.

4- Finish him!

Below you can find my implementation, I return HTTP status code 429 if limit is exceeded and inform client about the remaining request limit if it is not exceeded.

public boolean preHandle(HttpServletRequest request,  
                             HttpServletResponse response, Object handler)
            throws Exception {


        if (request.getMethod() == "POST" && rateLimitService.incrementLimit("POST~" + request.getRemoteAddr()) > 15) {
             response.sendError(429, "Rate limit exceeded, wait for the next quarter minute");
             return false;
        } else if (request.getMethod() == "GET" && rateLimitService.incrementLimit("GET~" + request.getRemoteAddr()) > 150) {
             response.sendError(429, "Rate limit exceeded, wait for the next quarter minute");
             return false;
        }
        response.addIntHeader("Remaining request count", (int) (150 - count));
        return true;
    }

You can implement your own rate limit in 60 minutes!

  • 30 mins cache start on AWS or 10 mins of brew install on Mac
  • 15 mins of google search for cache, code implementation
  • 15 mins of code implementation

Feel free to warn me about the wrong implementation! Ciao


Soner ALTIN
TAGGED IN work, aws, cloud computing, Java