Redis rate limiter in Spring Boot

Upasana | August 23, 2020 | 4 min read | 698 views


In this article we will demonstrate how to build a production grade basic rate limiter feature using Redis in Spring Boot 2.3 based application with Spring data Redis module. Rate limiter can be used in throttling API requests to a given service.

Basic Rate Limiting

The basic concept is that we want to limit number of requests to a particular service in a given time period. Basic Rate Limiting is an essential feature in any production grade API where we want to limit number of API calls per user per hour (or per minute). Few real world examples could be:

  • Allow 5 OTP requests from a given mobile number in 1 hour

  • Allow 5 Forgot password requests on website per hour

  • Allow 20 requests to a service using a given API key (as per plan)

  • Allow user (or IP address) to post max one comment per minute on a blogging site

Pseudocode

In this tutorial we will build a basic rate limiting feature that allows 10 requests/hour to our service for a logged in user. Redis provides two key commands namely - INCR and EXPIRE that will allow us build this features without much effort.

We will create a Redis key for every hour per username and make sure we expire that key after 1 hour so that we don’t fill up our entire database will stale data.

For username = carvia, the tabular presentation of Redis key will be something like this:

Redis Key implementation for Basic Rate Limiting

Time

11:00

12:00

13:00

14:00

Redis Key (string)

carvia:11

carvia:12

carvia:13

carvia:14

Value

3

5

10 (max limit)

null

Expires At

13:00 (2 hours later)

14:00

15:00

16:00

The Redis Key in this case is derived from username concatenated with the hour number by a colon. And we will always be expiring the key after 1 hour, so we do not need to worry about the junk accumulation in Redis store.

Pseudocode steps for Basic Rate limiter
  1. GET [username]:[current hour]

  2. If the result from step 1 exists and its value is less than 10, go to step 4 otherwise step 3

  3. Show error that max limit is reached. Exit

  4. Start a multi transaction in Redis and do the following:

    • Increment the counter for [username]:[current hour] using INCR command

    • Set expiry for the key to 2 hours from now using EXPIRE [username]:[current hour] 3600

  5. Allow request to proceed to service

Spring Data Redis

Spring Data Redis provides easy configuration and access to Redis from Spring applications offering both low-level and high-level abstractions for interacting with redis store.

We will create Spring Data Redis based implementation for the basic rate limiting feature in this tutorial.

Create Spring Boot Project

Head on to https://start.spring.io/ to create a new starter project choosing Spring data redis.

Your build.gradle should have the below entries:

plugins {
	id 'org.springframework.boot' version '2.3.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

group = 'com.carvia'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

Spring data redis based implementation

Make sure that you have Redis installed on your system before running this program. We will provide configuration for Redis in application.properties

/src/main/resources/application.properties
spring.redis.host=localhost
spring.redis.database=0
spring.redis.password=

That’s all we need to kick in Spring Boot auto configuration for Redis. Spring Boot automatically configures StringRedisTemplate that you can use to interact with Redis store.

Now it’s time to code the implementation for actual rate limiter that we were talking about.

/src/main/java/…​/RedisRateLimiter.java
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class RedisRateLimiter {

    private static final Logger logger = LoggerFactory.getLogger(RedisRateLimiter.class);

    @Autowired
    private StringRedisTemplate stringTemplate;

    private static final int REQUESTS_PER_HOUR = 10;

    public boolean isAllowed(String username) {
        final int hour = LocalDateTime.now().getHour(); (1)
        String key = username + ":" + hour;
        ValueOperations<String, String> operations = stringTemplate.opsForValue();
        String requests = operations.get(key);
        if (StringUtils.isNotBlank(requests) && Integer.parseInt(requests) >= REQUESTS_PER_HOUR) {
            return false;
        }
        List<Object> txResults = stringTemplate.execute(new SessionCallback<>() {
            @Override
            public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
                final StringRedisTemplate redisTemplate = (StringRedisTemplate) operations;
                final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
                operations.multi();
                valueOperations.increment(key);
                redisTemplate.expire(key, 2, TimeUnit.HOURS);
                // This will contain the results of all operations in the transaction
                return operations.exec();
            }
        });
        logger.info("Current request count: " + txResults.get(0));
        return true;
    }

    public void service() {
        for (int i=0; i<20; i++) {
            boolean allowed = isAllowed("carvia");
            if(!allowed)
                break;
        }
    }
}
1 We get the current hour value using Java 8 time API

Keys points to note here:

  • Getting value for a non-existence key returns null

  • We are using ValueOperations for interacting with String based keys in Redis

  • We are using SessionCallback to execute two commands in single atomic operation (Redis Transaction) - INCR and EXPIRE. Default RedisTemplate does not provide any guarantee that two consecutive operations will run on the same connection, SessionCallback is the reliable way of handling transactions.


Top articles in this category:
  1. Custom TTL for Spring data Redis Cache
  2. Sendgrid Dynamic Templates with Spring Boot
  3. Feign RequestInterceptor in Spring Boot
  4. Setting a Random Port in Spring Boot Application at startup
  5. Basic Auth Security in Spring Boot 2
  6. SendGrid emails in Spring Boot
  7. Testing web layer in Spring Boot using WebMvcTest

Recommended books for interview preparation:

Find more on this topic:
Buy interview books

Java & Microservices interview refresher for experienced developers.