Tuesday 5 July 2022

Rate limit a spring boot API using Bucket4j

In this post, I am going to explain how to rate limit a spring boot API using Bucket4j.

 

Bucket4j is a Java rate-limiting library based on token-bucket algorithm.

 

What is Token bucket algorithm

A token bucket add or generate tokens at some fixed rate. For example, I can create a token bucket of size 10 and a refill rate of 1 per 10 seconds. This refill happen only when the number of token in the bucket are < 10.

 

Let us try to understand this with below example. 

Time      Requests    AvailableTokens
5:00:00     0          10 (we have 10 tokens initially)
5:00:10     0          10 (refill attempt failed cause Bucket is full)
5:00:20     0          10 (refill attempt failed cause Bucket is full)
5:00:30     0          10 (refill attempt failed cause Bucket is full)
5:00:40     0          10 (refill attempt failed cause Bucket is full)
5:00:50     0          10 (refill attempt failed cause Bucket is full)
5:00:58     8          2 (10 - 8) 
5:01:00     0          3 (refill 1 token)
5:01:10     0          4 (refill 1 token)
5:01:20     0          5 (refill 1 token)
5:01:30     0          6 (refill 1 token)
5:01:49     4          2 (6 - 4) 
5:01:50     0          3 (refill 1 token)

 

When a client send request to the API, application remove the token from bucket if it is available and accept the request. Request is rejected when the bucket do not have any tokens.

 

Define a bucket

Bandwidth limit = Bandwidth.classic(5, Refill.greedy(2, Duration.ofSeconds(30)));
Bucket bucket = Bucket.builder().addLimit(limit).build();

 

Above definition set the bucket capacity to 5 and refill the tokens in a greedy manner (2 tokens for every 30 seconds). For example refill "10 tokens per 1 second" will add 1 token per each 100 millisecond, in other words refill will not wait 1 second to regenerate whole bunch of 10 tokens.

 

Apply the token to REST API

@RequestMapping("/hello")
public ResponseEntity<String> home() {

	System.out.println("Availabel tokens : " + bucket.getAvailableTokens());

	if (bucket.tryConsume(1)) {
		return ResponseEntity.ok("Hello!!!!");
	}

	return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header("Retry-After", "10").build();

}

 

'tryConsume(long numTokens)' tries to consume a specified number of tokens from this bucket and return true if the tokens were consumed, false otherwise.

 

Find the below working application.

 

Step 1: Create new maven project ‘bucket4j-hello-world’.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.sample.app</groupId>
	<artifactId>bucket4j-hello-world</artifactId>
	<version>1</version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.6</version>
	</parent>

	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.github.vladimir-bukhtoyarov</groupId>
			<artifactId>bucket4j-core</artifactId>
			<version>7.5.0</version>
		</dependency>


	</dependencies>
</project>

 

Step 3: Define HelloController class.

 

HelloController.java

package com.sample.app.controller;

import java.time.Duration;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

@RestController
public class HelloController {

	private final Bucket bucket;

	public HelloController() {
		Bandwidth limit = Bandwidth.classic(5, Refill.greedy(2, Duration.ofSeconds(30)));
		bucket = Bucket.builder().addLimit(limit).build();
	}

	@RequestMapping("/hello")
	public ResponseEntity<String> home() {

		System.out.println("Availabel tokens : " + bucket.getAvailableTokens());

		if (bucket.tryConsume(1)) {
			return ResponseEntity.ok("Hello!!!!");
		}

		return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header("Retry-After", "10").build();

	}
}

Step 4: Define main application class.

 

App.java

package com.sample.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
	public static void main(String[] args) {

		SpringApplication.run(App.class, args);

	}
}

Total project structure looks like below.





Run App.java.

 

Open the url ‘http://localhost:8080/hello’ in browser.

 

 


 

When the bucket has no token left, you will get http response code 429.

 


 

You can download complete working application from this link.


Previous                                                 Next                                                 Home

No comments:

Post a Comment