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.
<artifactId>spring-boot-
</dependency>
<!– Spring Data Redis –>
<dependency>
<groupId>org.springframework.
<artifactId>spring-boot-
</dependency>
<!– For annotations like @RateLimiter (custom implementation later) –>
<dependency>
<groupId>org.springframework.
<artifactId>spring-boot-
</dependency>
<!– Optional: Jackson for JSON handling –>
<dependency>
<groupId>com.fasterxml.
<artifactId>jackson-databind</
</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.
import org.springframework.
import java.util.concurrent.TimeUnit;
@Service
public class RedisRateLimiterService {
private final StringRedisTemplate redisTemplate;
public RedisRateLimiterService(
this.redisTemplate = redisTemplate;
}
public boolean isAllowed(String key, int limit, int windowSeconds) {
String redisKey = “rate_limiter:” + key;
Long current = redisTemplate.opsForValue().
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.
import jakarta.servlet.http.
import jakarta.servlet.http.
import org.springframework.
import org.springframework.web.
import java.io.IOException;
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final RedisRateLimiterService rateLimiterService;
public RateLimitingFilter(
this.rateLimiterService = rateLimiterService;
}
@Override
protected void doFilterInternal(
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.
response.setStatus(
response.getWriter().write(“
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.
import org.springframework.web.bind.
@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.
import org.springframework.
import java.time.Instant;
@Service
public class SlidingWindowRateLimiter {
private final StringRedisTemplate redisTemplate;
public SlidingWindowRateLimiter(
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().
// Count current requests
Long count = redisTemplate.opsForZSet().
if (count != null && count >= limit) {
return false;
}
// Add current request
redisTemplate.opsForZSet().
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(
this.redisTemplate = redisTemplate;
}
public boolean allowRequest(String key, int minIntervalSeconds) {
String redisKey = “throttle:” + key;
Long lastTime = redisTemplate.opsForValue().
long now = Instant.now().getEpochSecond()
if (lastTime != null && (now – lastTime) < minIntervalSeconds) {
return false;
}
redisTemplate.opsForValue().
return true;
}
}
This enforces a delay between successive requests. Useful for APIs like payments or OTP verification.
Best Practices for Rate Limiting and Throttling
- Granularity: Apply limits per API key, not just per IP.
- Flexible Configuration: Different endpoints may require different limits (e.g., /login stricter than /products).
- Custom Responses: Return headers like X-Rate-Limit-Remaining to help clients self-regulate.
- Distributed Environment: Redis ensures consistency across multiple application instances.
- Fallback Strategy: Instead of blocking, consider queuing or degrading service gracefully.
- 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.
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.