Spring Boot Rate Limiting using Bucket4j

Feb 07, 2023
Spring Boot Java

Rate limiting is used to put a cap on how many requests can be made to an API. Rate limiting protects an API from abuse such as brute-force login attempts or large volumes of requests to an expensive endpoint.

Rate limits can be applied to different scopes for example all requests, requests from a particular ip address, or requests by a particular user. Different endpoints can have different limits for example /login might have a lower limit than other endpoints.

In this article, we’ll use Bucket4j Spring Boot Starter to add rate limiting to a Spring Boot application. Bucket4j Spring Boot Starter allows rate limiting to be added to a Spring Boot through configuration only, no need to write any code or annotate your controller.

Check out the full example on GitHub https://github.com/minibuildsio/rate-limiting-example.

Required Dependencies

The first four dependencies are required to set up the rate limiting. The last two import the Caffeine caching library a high-performance non-distributed cache, you can use other providers for example Ehcast or Redis if you require a distributed cache.

implementation 'com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.7.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'javax.cache:cache-api:1.1.1'

implementation 'com.github.ben-manes.caffeine:caffeine:2.8.2'
implementation 'com.github.ben-manes.caffeine:jcache:2.8.2'

Note: Above is compatible with Spring Boot 2.7.8, check the bucket4j-spring-boot-starter compatibility table to find the version required for your version of Spring Boot.

Enable Caching

Add the @EnableCaching annotation to the application class or a @Configuration class. This will register the cache manager required by bucket4j-spring-boot-starter.

Configuration of the Cache and Rate Limits

bucket4j-spring-boot-starter requires a cache to keep track of the number of requests over a time period. The configuration below sets up Spring to use Caffeine, creates a cache called rate-limit-bucket, and sets the max size of the cache.

The example config below is in yaml form i.e. added to application.yaml but can be converted to properties if preferred.

spring:
  cache:
    jcache:
      provider: com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
    cache-names:
      - rate-limit-bucket
    caffeine:
      spec: maximumSize=100000,expireAfterAccess=3600s

The configuration below sets up a limit of 3 requests per 20 minutes per IP address for the /login endpoint and a limit of 20 requests per minute per IP address for any other endpoint.

bucket4j:
  enabled: true
  filters:
    - cache-key: getRemoteAddr()
      cache-name: rate-limit-bucket
      url: /login
      rate-limits:
        - bandwidths:
            - capacity: 3
              time: 20
              unit: minutes
    - cache-key: getRemoteAddr()
      cache-name: rate-limit-bucket
      url: ^((?!/login).)*$
      rate-limits:
        - bandwidths:
            - capacity: 20
              time: 1
              unit: minutes
  • cache-key: A Spring Expression Language expression that defines the cache key. getRemoteAddr() is simply the IP address of the requester more complex expressions can be used e.g. @securityService.username() != null ? @securityService.username() : getRemoteAddr().
  • cache-name: Refers to one of the caches created in the previous config.
  • url: A regular expression to match the endpoint.
  • capacity, time, unit: No of requests per time interval.

This example covers the most common use of rate limiting, more detail can be found here.

Test the Rate Limiting

We can test the rate-limiting in a regular @SpringBootTest by making the max number of requests of a given endpoint that should return ok (200) then making one more which should return too many requests (429).

@SpringBootTest
@AutoConfigureMockMvc
class StuffControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @Test
  void login_endpoint_is_restricted_to_3_requests_in_a_short_time() throws Exception {
    for (int i = 0; i < 3; i++) {
      mockMvc.perform(post("/login")).andExpect(status().isOk());
    }

    mockMvc.perform(post("/login")).andExpect(status().isTooManyRequests());
  }

  @Test
  void stuff_endpoint_is_restricted_to_20_requests_in_a_short_time() throws Exception {
    for (int i = 0; i < 20; i++) {
      mockMvc.perform(get("/stuff")).andExpect(status().isOk());
    }

    mockMvc.perform(get("/stuff")).andExpect(status().isTooManyRequests());
  }
}

Resetting the Rating Limit between Tests

When you are running multiple tests that call the same endpoint you will want to reset the rate limiting. To do this you can inject the cache manager used by the rate-limiting and clear it before every test.

  @Autowired
  CacheManager cacheManager;

  @BeforeEach
  void setUp() {
    cacheManager.getCache("rate-limit-bucket").clear();
  }

Disabling Rate Limiting

If you need to disable the rate-limiting completely in a test you can disable the cache in the application configuration.

spring:
  cache:
    type: none