Spring Boot Rate Limiting — API Throttling with Bucket4j
In this tutorial, you will learn about Spring Boot Rate Limiting. We cover key concepts, practical examples, and best practices to help you master this topic.
Bucket4j is a Java token-bucket rate limiting library that integrates with Spring Boot through interceptors and filters, supporting both local and distributed (Redis) rate limiting with bandwidth configurations.
What You'll Learn
- How to integrate Bucket4j with Spring Boot
- How to configure per-user and per-API rate limits
- How to use Redis for distributed Bucket4j limiting
Why It Matters
Spring Boot is the leading Java web framework. Bucket4j provides a robust token-bucket implementation with features like refill rates, overdraft, and time-window configuration, making it ideal for production Java rate limiting.
Real-World Use
DodaTech's Spring Boot microservice API uses Bucket4j with Redis for distributed rate limiting: 10 tokens/minute for free tier, 100 tokens/minute for Pro tier, and 1000 tokens/minute for Enterprise tier. Each request consumes one token, and tokens refill at the configured rate.
flowchart LR
A["HTTP Request"] --> B["Spring Boot\nInterceptor"]
B --> C["Bucket4j\nToken Check"]
C --> D{"Tokens\navailable?"}
D -->|"Yes - consume token"| E["Controller\nMethod"]
D -->|"No"| F["429 Too Many\nRequests"]
E --> G["Response"]
style B fill:#dbeafe,stroke:#2563eb
style C fill:#fef3c7,stroke:#d97706
style F fill:#fecaca,stroke:#dc2626
Bucket4j Configuration
// RateLimitConfig.java
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class RateLimitConfig {
public Bucket createBucket(int capacity, int refillTokens, Duration refillDuration) {
Bandwidth limit = Bandwidth.classic(capacity, Refill.greedy(refillTokens, refillDuration));
return Bucket.builder().addLimit(limit).build();
}
@Bean
public Bucket freeTierBucket() {
return createBucket(10, 10, Duration.ofMinutes(1));
}
@Bean
public Bucket proTierBucket() {
return createBucket(100, 100, Duration.ofMinutes(1));
}
}
Rate Limiting Interceptor
// RateLimitInterceptor.java
import io.github.bucket4j.Bucket;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> bucketCache = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String apiKey = request.getHeader("X-API-Key");
if (apiKey == null) {
response.setStatus(401);
response.getWriter().write("{\"error\":\"missing_api_key\"}");
return false;
}
Bucket bucket = bucketCache.computeIfAbsent(apiKey,
k -> createBucketForTier(getTier(apiKey)));
if (bucket.tryConsume(1)) {
response.setHeader("X-RateLimit-Remaining",
String.valueOf(bucket.getAvailableTokens()));
return true;
}
response.setStatus(429);
response.setHeader("X-RateLimit-Retry-After", "60");
response.getWriter().write(
"{\"error\":\"rate_limit_exceeded\",\"retry_after\":60}"
);
return false;
}
private String getTier(String apiKey) {
// Lookup tier from database
if (apiKey.startsWith("free")) return "free";
if (apiKey.startsWith("pro")) return "pro";
return "enterprise";
}
private Bucket createBucketForTier(String tier) {
return switch (tier) {
case "free" -> createBucket(10, 10, Duration.ofMinutes(1));
case "pro" -> createBucket(100, 100, Duration.ofMinutes(1));
default -> createBucket(1000, 1000, Duration.ofMinutes(1));
};
}
}
Redis-Backed Distributed Bucket4j
// RedisBucketConfig.java
import io.github.bucket4j.distributed.ExpirationAfterWriteStrategy;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.redis.jedis.cas.JedisProxyManager;
import redis.clients.jedis.JedisPool;
@Configuration
public class RedisBucketConfig {
@Bean
public JedisPool jedisPool() {
return new JedisPool("redis-cluster.dodatech.com", 6379);
}
@Bean
public ProxyManager<String> proxyManager(JedisPool jedisPool) {
return JedisProxyManager.builderFor(jedisPool)
.withExpirationStrategy(
ExpirationAfterWriteStrategy
.unlimited()
)
.build();
}
}
Common Mistakes
1. Creating a New Bucket per Request
Buckets should be cached and reused per key. Creating a new bucket for each request bypasses rate limiting.
2. Using Local Buckets in Distributed Deployments
Local buckets are not shared across instances. Use Redis-backed ProxyManager for distributed rate limiting.
3. Not Handling Token Consumption Errors
If bucket operations throw exceptions, your rate limiter fails open (allows all requests). Implement proper error handling.
4. Forgetting to Configure CORS Preflight
Rate limits on OPTIONS requests can break CORS. Skip rate limiting for preflight requests.
5. Not Testing Token Refill Behavior
Verify that tokens refill at the expected rate. Greedy refill vs intervally refill have different behaviors.
Practice Questions
- What rate limiting algorithm does Bucket4j implement?
- How do you cache buckets per API key?
- Why is Redis needed in multi-instance deployments?
- What is greedy refill in Bucket4j?
- How do you add rate limit headers to responses?
Answers
- Token bucket algorithm. 2. Use a ConcurrentHashMap or Redis-backed ProxyManager. 3. To share rate limit state across application instances. 4. Tokens are added continuously rather than at fixed intervals. 5. Use response.setHeader() with available token count.
Challenge
Build a Spring Boot application with Bucket4j rate limiting that: supports per-API-key rate limits with tiered configuration, uses Redis for distributed state, includes custom response headers, handles CORS preflight correctly, and provides a management endpoint to view current bucket states.
FAQ
Mini Project
Create a complete Spring Boot application with: per-API-key rate limiting using Bucket4j and Redis, three tiers (free/pro/enterprise) with different limits, rate limit headers on all responses, a health endpoint that bypasses rate limiting, and a Grafana dashboard tracking rate limit metrics exported via Micrometer.
What's Next
- Learn about FastAPI rate limiting with SlowAPI
- Explore distributed rate limiting with Redis cluster
- Continue to the rate limiting project combining all concepts
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro