APIs have become the backbone of modern applications, enabling seamless communication across microservices, mobile apps, and third-party integrations. But as APIs become more exposed, they face challenges such as overload, brute-force attacks, and accidental misuse by clients. To ensure stability, scalability, and security, it’s crucial to implement rate limiting and throttling mechanisms.

In this article, we’ll walk through building a rate limiter and throttling layer in Spring Boot with Redis, covering both concepts in depth and providing working code examples. By the end, you’ll have a solid understanding of how to safeguard your APIs effectively.

Understanding Rate Limiting and Throttling

Before we dive into implementation, let’s clarify some key concepts:

  • Rate Limiting: Restricts the number of requests a client can make in a given timeframe (e.g., 100 requests per minute). This prevents overload and brute-force attacks.
  • Throttling: Controls the flow of requests over time, ensuring that requests are spread out evenly and not executed in bursts.
  • Use Cases:
    • Protecting APIs from abusive clients.
    • Preventing accidental denial of service caused by misconfigured clients.
    • Enforcing fairness among multiple consumers.
    • Securing login endpoints from brute-force attacks.

Redis is a perfect fit for this because of its atomic operations, low latency, and support for expiry times, which make tracking request counts efficient.

Project Setup

We’ll create a Spring Boot application with Redis integration.

Dependencies (Gradle or Maven):

<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!– Spring Data Redis –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!– For annotations like @RateLimiter (custom implementation later) –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!– Optional: Jackson for JSON handling –>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>

application.yml configuration for Redis:

spring:
redis:
host: localhost
port: 6379
timeout: 60000

Make sure Redis is installed and running locally or via Docker:

docker run --name redis -d -p 6379:6379 redis

Designing the Rate Limiter

We’ll use a fixed window counter strategy first, where each client’s request count is stored in Redis with an expiration window.

Redis Rate Limiter Service

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class RedisRateLimiterService {

private final StringRedisTemplate redisTemplate;

public RedisRateLimiterService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

public boolean isAllowed(String key, int limit, int windowSeconds) {
String redisKey = “rate_limiter:” + key;

Long current = redisTemplate.opsForValue().increment(redisKey);

if (current == 1) {
redisTemplate.expire(redisKey, windowSeconds, TimeUnit.SECONDS);
}

return current <= limit;
}
}

  • Each request increments a Redis counter.
  • On first request, Redis sets an expiration time for that key.
  • If the counter exceeds the limit, requests are denied.

Rate Limiting Filter

We’ll add a filter that checks incoming requests before they hit the controller.

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@Component
public class RateLimitingFilter extends OncePerRequestFilter {

private final RedisRateLimiterService rateLimiterService;

public RateLimitingFilter(RedisRateLimiterService rateLimiterService) {
this.rateLimiterService = rateLimiterService;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

String clientIp = request.getRemoteAddr();
String apiPath = request.getRequestURI();

// Unique key per client + endpoint
String key = clientIp + “:” + apiPath;

if (!rateLimiterService.isAllowed(key, 5, 60)) { // 5 requests per 60s
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.getWriter().write(“Too many requests – try again later”);
return;
}

filterChain.doFilter(request, response);
}
}

This filter applies a per-client, per-endpoint rate limit. You can customize based on API keys or authentication tokens.

Applying to Controllers

Here’s a simple controller that gets protected by the filter:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ApiController {

@GetMapping(“/api/data”)
public String getData() {
return “Here is your protected data!”;
}

@GetMapping(“/api/login”)
public String login() {
return “Login endpoint”;
}
}

If a client sends more than 5 requests per minute to /api/data, they’ll be blocked.

Sliding Window Approach (Optional Enhancement)

The fixed window strategy has edge cases (bursts at window boundaries). A sliding window is more accurate. We can implement it by using Redis’ sorted sets.

Example: store request timestamps and count only those within the last N seconds.

import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import java.time.Instant;

@Service
public class SlidingWindowRateLimiter {

private final StringRedisTemplate redisTemplate;

public SlidingWindowRateLimiter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

public boolean isAllowed(String key, int limit, int windowSeconds) {
String redisKey = “sliding_rate:” + key;
long now = Instant.now().getEpochSecond();

// Remove expired requests
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, now – windowSeconds);

// Count current requests
Long count = redisTemplate.opsForZSet().zCard(redisKey);

if (count != null && count >= limit) {
return false;
}

// Add current request
redisTemplate.opsForZSet().add(redisKey, String.valueOf(now), now);
redisTemplate.expire(redisKey, windowSeconds, TimeUnit.SECONDS);

return true;
}
}

This method prevents “bursting” at window resets and provides smoother traffic control.

Applying Throttling

While rate limiting restricts the total number of requests, throttling ensures even distribution of requests. For example, no more than 1 request every 2 seconds.

Implementation:

@Service
public class ThrottlingService {

private final StringRedisTemplate redisTemplate;

public ThrottlingService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

public boolean allowRequest(String key, int minIntervalSeconds) {
String redisKey = “throttle:” + key;
Long lastTime = redisTemplate.opsForValue().increment(redisKey, 0);

long now = Instant.now().getEpochSecond();

if (lastTime != null && (now – lastTime) < minIntervalSeconds) {
return false;
}

redisTemplate.opsForValue().set(redisKey, String.valueOf(now));
return true;
}
}

This enforces a delay between successive requests. Useful for APIs like payments or OTP verification.

Best Practices for Rate Limiting and Throttling

  1. Granularity: Apply limits per API key, not just per IP.
  2. Flexible Configuration: Different endpoints may require different limits (e.g., /login stricter than /products).
  3. Custom Responses: Return headers like X-Rate-Limit-Remaining to help clients self-regulate.
  4. Distributed Environment: Redis ensures consistency across multiple application instances.
  5. Fallback Strategy: Instead of blocking, consider queuing or degrading service gracefully.
  6. Monitoring: Track rate-limited events for anomaly detection.

Login Endpoint Protection

To prevent brute-force login attempts:

@PostMapping("/api/login")
public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
String clientKey = request.getRemoteAddr() + ":" + username;

if (!rateLimiterService.isAllowed(clientKey, 3, 300)) { // 3 attempts per 5 minutes
return “Too many failed login attempts. Try again later.”;
}

// authenticate user…
return “Login success (if credentials are correct)”;
}

This ensures a malicious actor can’t spam the login endpoint.

Conclusion

In today’s API-driven world, ensuring the resilience and security of your endpoints is non-negotiable. By combining rate limiting and throttling in Spring Boot with Redis, you can:

  • Prevent API overload caused by misbehaving clients.
  • Protect against brute-force attacks on sensitive endpoints.
  • Ensure fair usage among all consumers.
  • Maintain system performance under heavy load.

We explored both fixed window counters and sliding window algorithms, along with practical code examples for Spring Boot. Additionally, we saw how throttling can smooth out traffic flow and how specific endpoints like login can benefit from stricter limits.

The key takeaway is that rate limiting is not one-size-fits-all. You should carefully design limits based on endpoint criticality, user roles, and business needs. Redis, with its atomic operations and expiration support, provides a robust foundation for building such a system in distributed environments.

By implementing these practices, you not only strengthen the security of your APIs but also enhance the user experience by preventing accidental service degradation. In a world of increasing API consumption, a reliable rate limiter is your first line of defense.