Rate limiting is a critical pattern for building resilient, secure, and fair APIs and applications. It protects systems from abuse, ensures fair resource allocation, and maintains quality of service under heavy load. This article explores various rate limiting algorithms and their implementation in Java, from simple in-memory solutions to distributed systems.
Why Rate Limiting?
Common Use Cases:
- API Protection: Prevent DDoS attacks and API abuse
- Cost Control: Manage third-party API usage to control costs
- Fair Usage: Ensure equitable resource distribution among users
- Load Shedding: Prevent system overload during traffic spikes
- Compliance: Meet API rate limiting requirements for partners
Rate Limiting Algorithms
1. Fixed Window Counter
- Divides time into fixed windows (e.g., 1 minute)
- Counts requests in current window
- Simple but allows bursts at window boundaries
2. Sliding Window Log
- Maintains timestamps of recent requests
- Accurate but memory-intensive
3. Token Bucket
- Tokens are added at a fixed rate
- Requests consume tokens
- Allows bursts up to bucket capacity
4. Leaky Bucket
- Requests enter a queue at variable rate
- Processed at fixed rate
- Smooths out traffic bursts
1. Guava RateLimiter (Token Bucket)
Google's Guava library provides a simple, in-memory rate limiter based on the token bucket algorithm.
Maven Dependency:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency>
Basic Usage:
import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiterExample {
public static void main(String[] args) {
// Create a rate limiter that allows 10 requests per second
RateLimiter rateLimiter = RateLimiter.create(10.0); // 10 permits per second
for (int i = 0; i < 20; i++) {
// Acquire a permit, blocking if necessary
double waitTime = rateLimiter.acquire();
System.out.printf("Request %d - Waited: %.2f seconds%n", i + 1, waitTime);
processRequest(i);
}
}
private static void processRequest(int requestId) {
System.out.println("Processing request: " + requestId);
}
}
Advanced Configuration:
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
public class AdvancedGuavaRateLimiter {
private final RateLimiter rateLimiter;
public AdvancedGuavaRateLimiter(double permitsPerSecond) {
this.rateLimiter = RateLimiter.create(permitsPerSecond);
}
public AdvancedGuavaRateLimiter(double permitsPerSecond, Duration warmupPeriod) {
this.rateLimiter = RateLimiter.create(permitsPerSecond,
warmupPeriod.toMillis(), TimeUnit.MILLISECONDS);
}
// Try to acquire without waiting
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
// Try to acquire with timeout
public boolean tryAcquire(long timeout, TimeUnit unit) {
return rateLimiter.tryAcquire(timeout, unit);
}
// Acquire multiple permits
public double acquire(int permits) {
return rateLimiter.acquire(permits);
}
public double getRate() {
return rateLimiter.getRate();
}
public void setRate(double permitsPerSecond) {
rateLimiter.setRate(permitsPerSecond);
}
}
// Usage example
class GuavaRateLimiterDemo {
public static void main(String[] args) {
// Create with warmup period
AdvancedGuavaRateLimiter limiter = new AdvancedGuavaRateLimiter(
100.0, Duration.ofSeconds(30));
// Dynamic rate adjustment
limiter.setRate(50.0); // Adjust rate at runtime
for (int i = 0; i < 10; i++) {
if (limiter.tryAcquire()) {
System.out.println("Request " + i + " processed immediately");
} else {
System.out.println("Request " + i + " rejected - rate limit exceeded");
}
}
}
}
2. Resilience4j Rate Limiter
Resilience4j provides a more enterprise-ready solution with additional features like metrics and integration with other resilience patterns.
Maven Dependencies:
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-micrometer</artifactId> <version>2.1.0</version> </dependency>
Basic Implementation:
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import java.time.Duration;
import java.util.function.Supplier;
public class Resilience4jRateLimiterExample {
public static void main(String[] args) {
// Create rate limiter configuration
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(50) // 50 requests
.limitRefreshPeriod(Duration.ofSeconds(1)) // per second
.timeoutDuration(Duration.ofMillis(100)) // wait time
.build();
// Create rate limiter
RateLimiter rateLimiter = RateLimiter.of("api-service", config);
// Decorate your function
Supplier<String> restrictedSupplier = RateLimiter.decorateSupplier(
rateLimiter, () -> callExternalService());
for (int i = 0; i < 10; i++) {
try {
String result = restrictedSupplier.get();
System.out.println("Request " + i + ": " + result);
} catch (Exception e) {
System.out.println("Request " + i + " blocked: " + e.getMessage());
}
}
}
private static String callExternalService() {
return "Service response";
}
}
Advanced Configuration with Registry:
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
public class AdvancedResilience4jRateLimiter {
private final RateLimiterRegistry registry;
private final AtomicInteger successCount = new AtomicInteger();
private final AtomicInteger blockedCount = new AtomicInteger();
public AdvancedResilience4jRateLimiter() {
// Create registry with custom configuration
this.registry = RateLimiterRegistry.of(createDefaultConfig());
// Add metrics
registry.rateLimiter("api-service")
.getEventPublisher()
.onSuccess(event -> successCount.incrementAndGet())
.onFailure(event -> blockedCount.incrementAndGet());
}
private RateLimiterConfig createDefaultConfig() {
return RateLimiterConfig.custom()
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ZERO) // Non-blocking
.writableStackTraceEnabled(false)
.build();
}
public RateLimiter createRateLimiter(String name, int limit, Duration refreshPeriod) {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(limit)
.limitRefreshPeriod(refreshPeriod)
.timeoutDuration(Duration.ZERO)
.build();
return registry.rateLimiter(name, config);
}
public <T> T executeWithRateLimit(String rateLimiterName, Supplier<T> supplier) {
RateLimiter rateLimiter = registry.rateLimiter(rateLimiterName);
try {
return rateLimiter.executeSupplier(supplier);
} catch (RequestNotPermitted e) {
System.out.println("Rate limit exceeded for " + rateLimiterName);
throw e;
}
}
public void printMetrics() {
System.out.println("Successful requests: " + successCount.get());
System.out.println("Blocked requests: " + blockedCount.get());
}
}
// Usage example
class Resilience4jDemo {
public static void main(String[] args) {
AdvancedResilience4jRateLimiter advancedLimiter = new AdvancedResilience4jRateLimiter();
// Create different rate limiters for different services
RateLimiter userServiceLimiter = advancedLimiter.createRateLimiter(
"user-service", 50, Duration.ofSeconds(1));
RateLimiter paymentServiceLimiter = advancedLimiter.createRateLimiter(
"payment-service", 10, Duration.ofSeconds(1));
// Execute with rate limiting
for (int i = 0; i < 15; i++) {
try {
String result = advancedLimiter.executeWithRateLimit(
"payment-service",
() -> processPayment(i)
);
System.out.println("Payment " + i + ": " + result);
} catch (RequestNotPermitted e) {
System.out.println("Payment " + i + " blocked by rate limiter");
}
}
advancedLimiter.printMetrics();
}
private static String processPayment(int paymentId) {
return "Processed payment " + paymentId;
}
}
3. Custom Fixed Window Rate Limiter
For simple use cases, you can implement your own rate limiter.
In-Memory Fixed Window Implementation:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FixedWindowRateLimiter {
private final int maxRequests;
private final long windowSizeInMillis;
private final ConcurrentHashMap<String, Window> store;
private final Lock lock;
public FixedWindowRateLimiter(int maxRequests, long windowSizeInMillis) {
this.maxRequests = maxRequests;
this.windowSizeInMillis = windowSizeInMillis;
this.store = new ConcurrentHashMap<>();
this.lock = new ReentrantLock();
}
public boolean allowRequest(String key) {
long currentTime = System.currentTimeMillis();
long windowKey = currentTime / windowSizeInMillis;
String compositeKey = key + ":" + windowKey;
// Use compute to atomically update the window
Window window = store.compute(compoundKey, (k, existingWindow) -> {
if (existingWindow == null || existingWindow.windowStart < windowKey) {
// New window or expired window
return new Window(windowKey, new AtomicInteger(1));
} else {
// Existing window - increment counter
existingWindow.counter.incrementAndGet();
return existingWindow;
}
});
return window.counter.get() <= maxRequests;
}
public boolean tryConsume(String key) {
lock.lock();
try {
if (allowRequest(key)) {
return true;
}
return false;
} finally {
lock.unlock();
}
}
public void cleanup() {
long currentTime = System.currentTimeMillis();
long currentWindow = currentTime / windowSizeInMillis;
store.entrySet().removeIf(entry -> {
Window window = entry.getValue();
return window.windowStart < currentWindow - 1; // Keep only current and previous window
});
}
private static class Window {
final long windowStart;
final AtomicInteger counter;
Window(long windowStart, AtomicInteger counter) {
this.windowStart = windowStart;
this.counter = counter;
}
}
}
// Usage example
class FixedWindowDemo {
public static void main(String[] args) throws InterruptedException {
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(5, 1000); // 5 requests per second
for (int i = 0; i < 10; i++) {
String userId = "user123";
if (limiter.allowRequest(userId)) {
System.out.println("Request " + i + " allowed");
} else {
System.out.println("Request " + i + " blocked");
}
Thread.sleep(100);
}
}
}
4. Spring Boot Rate Limiting with Annotations
Create a custom annotation for declarative rate limiting.
Rate Limit Annotation:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimited {
String key() default "";
int limit() default 100;
int duration() default 60; // seconds
String message() default "Rate limit exceeded";
}
Rate Limit Aspect:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(rateLimited)")
public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimited rateLimited) throws Throwable {
String key = buildRateLimitKey(joinPoint, rateLimited);
int limit = rateLimited.limit();
int duration = rateLimited.duration();
ValueOperations<String, String> ops = redisTemplate.opsForValue();
// Use Redis atomic operations for rate limiting
Long current = ops.increment(key, 1);
if (current == 1) {
// First request - set expiration
redisTemplate.expire(key, duration, TimeUnit.SECONDS);
}
if (current > limit) {
throw new RateLimitExceededException(rateLimited.message());
}
return joinPoint.proceed();
}
private String buildRateLimitKey(ProceedingJoinPoint joinPoint, RateLimited rateLimited) {
StringBuilder key = new StringBuilder("rate_limit:");
// Use custom key if provided
if (!rateLimited.key().isEmpty()) {
key.append(rateLimited.key());
} else {
// Build key from method and user
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
key.append(method.getDeclaringClass().getSimpleName())
.append(":")
.append(method.getName());
// Add user identifier if available
String userIdentifier = getUserIdentifier();
if (userIdentifier != null) {
key.append(":").append(userIdentifier);
}
}
return key.toString();
}
private String getUserIdentifier() {
try {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes()).getRequest();
// Use API key, user ID, or IP address
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) return "api_key:" + apiKey;
String userId = request.getHeader("X-User-ID");
if (userId != null) return "user:" + userId;
return "ip:" + request.getRemoteAddr();
} catch (Exception e) {
return "anonymous";
}
}
}
// Custom exception
class RateLimitExceededException extends RuntimeException {
public RateLimitExceededException(String message) {
super(message);
}
}
Service Usage:
@Service
public class ApiService {
@RateLimited(limit = 10, duration = 60, message = "Too many requests")
public String processApiCall(String data) {
// Business logic
return "Processed: " + data;
}
@RateLimited(key = "premium_feature", limit = 100, duration = 3600)
public String premiumFeature(String userId) {
// Premium feature logic
return "Premium result for " + userId;
}
@RateLimited(limit = 1000, duration = 3600) // 1000 requests per hour
public String highCapacityEndpoint() {
return "High capacity endpoint response";
}
}
5. Bucket4j Distributed Rate Limiting
For distributed systems, Bucket4j provides a robust solution with various backends.
Maven Dependencies:
<dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-core</artifactId> <version>8.10.0</version> </dependency> <dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-jcache</artifactId> <version>8.10.0</version> </dependency>
Redis-Backed Rate Limiter:
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.ExpirationAfterWrite;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import org.redisson.command.CommandSyncService;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.time.Duration;
import java.util.function.Supplier;
public class DistributedRateLimiter {
private final ProxyManager<String> proxyManager;
public DistributedRateLimiter(RedissonClient redissonClient) {
this.proxyManager = new RedisProxyManager(redissonClient);
}
public Bucket getBucket(String key, Bandwidth bandwidth) {
BucketConfiguration configuration = BucketConfiguration.builder()
.addLimit(bandwidth)
.build();
return proxyManager.builder()
.build(key, configuration);
}
public boolean tryConsume(String key, int tokens) {
Bucket bucket = createBucket(key);
return bucket.tryConsume(tokens);
}
private Bucket createBucket(String key) {
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)));
return getBucket(key, limit);
}
// Bandwidth factory methods
public static Bandwidth perMinute(int requests) {
return Bandwidth.classic(requests, Refill.intervally(requests, Duration.ofMinutes(1)));
}
public static Bandwidth perHour(int requests) {
return Bandwidth.classic(requests, Refill.intervally(requests, Duration.ofHours(1)));
}
public static Bandwidth perDay(int requests) {
return Bandwidth.classic(requests, Refill.intervally(requests, Duration.ofDays(1)));
}
}
// Usage example
class Bucket4jDemo {
public static void main(String[] args) {
// Configure Redis
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
DistributedRateLimiter rateLimiter = new DistributedRateLimiter(redisson);
// Different rate limits for different users
String user1 = "user:premium:123";
String user2 = "user:basic:456";
for (int i = 0; i < 15; i++) {
boolean allowed1 = rateLimiter.tryConsume(user1, 1);
boolean allowed2 = rateLimiter.tryConsume(user2, 1);
System.out.printf("Request %d - User1: %s, User2: %s%n",
i, allowed1 ? "ALLOWED" : "BLOCKED", allowed2 ? "ALLOWED" : "BLOCKED");
}
redisson.shutdown();
}
}
6. Configuration Best Practices
YAML Configuration for Spring Boot:
app: rate-limiting: enabled: true default-limit: 100 default-duration: 3600 limits: user-api: limit: 1000 duration: 3600 payment-api: limit: 100 duration: 60 admin-api: limit: 10000 duration: 3600 redis: host: localhost port: 6379 timeout: 2000ms # For Resilience4j resilience4j: ratelimiter: instances: user-service: limit-for-period: 100 limit-refresh-period: 1s timeout-duration: 0 register-health-indicator: true payment-service: limit-for-period: 10 limit-refresh-period: 1s timeout-duration: 100ms
Configuration Class:
@Configuration
@ConfigurationProperties(prefix = "app.rate-limiting")
@Data
public class RateLimitConfig {
private boolean enabled;
private int defaultLimit;
private int defaultDuration;
private Map<String, RateLimit> limits;
private RedisConfig redis;
@Data
public static class RateLimit {
private int limit;
private int duration;
}
@Data
public static class RedisConfig {
private String host;
private int port;
private Duration timeout;
}
}
Monitoring and Metrics
Micrometer Metrics with Resilience4j:
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
@Component
public class RateLimitMetrics {
private final MeterRegistry meterRegistry;
private final Timer successfulRequests;
private final Timer blockedRequests;
public RateLimitMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.successfulRequests = Timer.builder("rate.limiter.requests")
.tag("status", "success")
.register(meterRegistry);
this.blockedRequests = Timer.builder("rate.limiter.requests")
.tag("status", "blocked")
.register(meterRegistry);
}
public void recordSuccess(String serviceName, long duration) {
meterRegistry.counter("rate.limiter.success", "service", serviceName).increment();
successfulRequests.record(Duration.ofMillis(duration));
}
public void recordBlocked(String serviceName) {
meterRegistry.counter("rate.limiter.blocked", "service", serviceName).increment();
blockedRequests.record(Duration.ZERO);
}
}
Testing Rate Limiters
JUnit Tests:
@Testcontainers
@SpringBootTest
class RateLimiterTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Test
void testRateLimiterAllowsWithinLimit() {
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(5, 1000);
for (int i = 0; i < 5; i++) {
assertTrue(limiter.allowRequest("test-user"));
}
}
@Test
void testRateLimiterBlocksBeyondLimit() {
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(3, 1000);
// First 3 requests should pass
assertTrue(limiter.allowRequest("test-user"));
assertTrue(limiter.allowRequest("test-user"));
assertTrue(limiter.allowRequest("test-user"));
// Fourth should fail
assertFalse(limiter.allowRequest("test-user"));
}
@Test
void testDifferentUsersHaveSeparateLimits() {
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(2, 1000);
assertTrue(limiter.allowRequest("user1"));
assertTrue(limiter.allowRequest("user1"));
assertTrue(limiter.allowRequest("user2")); // Different user
assertTrue(limiter.allowRequest("user2"));
assertFalse(limiter.allowRequest("user1")); // user1 exceeded
assertTrue(limiter.allowRequest("user2")); // user2 still has capacity
}
}
Conclusion
Choosing the Right Rate Limiter:
| Use Case | Recommended Solution |
|---|---|
| Simple in-memory | Guava RateLimiter |
| Enterprise features | Resilience4j |
| Distributed systems | Bucket4j with Redis |
| Spring Boot APIs | Custom annotation with AOP |
| High performance | Fixed window with caching |
Best Practices:
- Start Simple: Begin with in-memory limiters for monoliths
- Go Distributed Early: Use Redis-backed solutions for microservices
- Monitor Everything: Track rate limit usage and effectiveness
- Use Meaningful Keys: Include user, service, and endpoint in rate limit keys
- Provide Clear Headers: Return rate limit information in HTTP headers
- Implement Graceful Degradation: Provide fallbacks when rate limits are hit
Rate limiting is essential for building resilient, secure, and fair applications. By choosing the right algorithm and implementation for your use case, you can protect your systems while providing a good user experience.