API Rate Limiter in Java Spring Boot Using Bucket4J
Overview
Rate limiting is a technique to limit an api resource to a certain number of usage in a certain number to time duration. Rate limiting can be done via various ways but crux of it is allowing user to use an api resource in a limited way that users does not abuse the api resource. Rate limit is the first mechanism to use when there is possibility of DDOS attack. This should ideally be used along with authentication and authorization.
Implementation
Lets talk code.
First of all add following, dependency to your existing spring maven project.
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>7.4.0</version>
</dependency>
This is Bucket4J maven dependency which contains necessary implementations for the rate limiting
Now create one controller class which exemplifies the api resource.
package com.hemindent.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.log4j.Log4j2;
@RestController
@Log4j2
public class TestController {
@GetMapping("/test")
ResponseEntity<String> sayHello() {
return ResponseEntity.ok("test");
}
@GetMapping("/error")
ResponseEntity<String> sayError() {
return ResponseEntity.ok("error");
}
}
Now lets quickly start he application and test the api resource by Postman. If everything is up to the mark you should see output like following.
This Api resource is currently un restricted nd does not have rate limiting. If you hit this url multiple times in a minute this api should be accessible easily. Lets just change that and add a rate limiting config that only allows 3 api hits per 1 Minutes.
Lets just add following Bucket4J Interceptor config to our project.
package com.hemindent.config;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Value("${rate.limit}")
private int RATE_LIMIT;
@Value("${time.duration.in.minutes}")
private long TIME_DURATION;
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
private Bucket newBucket(String apiKey) {
Refill refill = Refill.intervally(RATE_LIMIT, Duration.ofMinutes(TIME_DURATION));
Bandwidth limit = Bandwidth.classic(RATE_LIMIT, refill);
return Bucket.builder()
.addLimit(limit)
.build();
}
public Bucket resolveBucket(String apiKey) {
return cache.computeIfAbsent(apiKey, this::newBucket);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String remoteIpAddress = request.getHeader("X-Forwarded-For");
if (remoteIpAddress == null || remoteIpAddress.isEmpty()) {
remoteIpAddress = request.getRemoteAddr();
}
if (remoteIpAddress == null || remoteIpAddress.isEmpty()) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: remoteIpAddress");
return false;
}
Bucket tokenBucket = resolveBucket(remoteIpAddress);
ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.addHeader("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()));
return true;
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.addHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota");
return false;
}
}
}
Also add following properties to properties.applicaiton file.
rate.limit = 3
time.duration.in.minutes = 1
Now lets do last step require to configure this interceptor to our api resource.
package com.hemindent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class BucketAppConfig implements WebMvcConfigurer {
private final RateLimitInterceptor interceptor;
public BucketAppConfig(RateLimitInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
.addPathPatterns("/test");
}
}
This last step configures our /test
GET api to rate limiting interceptor.
After this restart the application, and hit the /test GET api for 4 times and you will get following error message indicating the rate limiting has hit the api resource. And after the 1 minute same api resource will work as normally.
You can also further change the timings of the configs. Also you can configure a certain user / user group to be limited only by further drilling down. But that is for other time.
Seeya.
Subscribe to my newsletter
Read articles from Hemin Panchal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hemin Panchal
Hemin Panchal
I ππ₯ instruct πcomputersπ² to do β¨ Human π€ Stuffs. And occasionally πΆ use π» my computer π± Degree to save π¦ the world. ππ