google-site-verification: google61fe8ba583a51912.html
Rate Limiting with Bucket4j in Java

Introduction

Bucket4j is a Java rate-limiting library based on the token-bucket algorithm. It provides a powerful and flexible way to control the rate of operations in distributed and standalone applications, preventing abuse and ensuring fair resource usage.

Core Concepts

Token Bucket Algorithm

The token bucket algorithm works by:

  • Bucket Capacity: Maximum number of tokens the bucket can hold
  • Refill Rate: How quickly tokens are replenished
  • Tokens: Each operation consumes one or more tokens
  • Blocking: Operations wait if insufficient tokens are available

Basic Setup and Dependencies

Maven Dependencies

<dependencies> <!-- Core Bucket4j library --> <dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-core</artifactId> <version>8.10.0</version> </dependency> <!-- For distributed caching --> <dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-jcache</artifactId> <version>8.10.0</version> </dependency> <!-- For Hazelcast backend --> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>5.3.6</version> </dependency> <!-- For Redis backend --> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.3.1.RELEASE</version> </dependency> </dependencies>

Basic Rate Limiting

Local In-Memory Rate Limiter

public class BasicRateLimiter { public void demonstrateBasicRateLimiting() { // Create a bandwidth: 10 tokens, refill 10 tokens every 1 minute Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1))); // Create bucket configuration BucketConfiguration configuration = BucketConfiguration.builder() .addLimit(limit) .build(); // Create bucket Bucket bucket = Bucket4j.builder() .addLimit(limit) .build(); // Usage examples for (int i = 1; i <= 15; i++) { if (bucket.tryConsume(1)) { System.out.println("Request " + i + ": Allowed"); } else { System.out.println("Request " + i + ": Rate limited"); } } } public void blockingOperations() throws InterruptedException { Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1))); Bucket bucket = Bucket4j.builder().addLimit(limit).build(); // Block until tokens available CompletableFuture<Boolean> future = bucket.asAsync().tryConsume(1); boolean consumed = future.get(); // blocks until token available // Or use blocking API bucket.asBlocking().consume(1); // blocks until token available } }

Advanced Configuration

Multiple Bandwidth Limits

public class AdvancedRateLimiting { public void multipleBandwidthLimits() { // Multiple limits: 1000 requests per hour AND 100 requests per minute Bucket bucket = Bucket4j.builder() .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1)))) .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) .build(); // Complex refill strategies Bandwidth greedyRefill = Bandwidth.classic(10, Refill.greedy(10, Duration.ofMinutes(1))); Bandwidth intervallyAligned = Bandwidth.classic(100, Refill.intervallyAligned(100, Duration.ofHours(1), Instant.now(), true)); } public void customBandwidthConfiguration() { // Bandwidth with custom parameters Bandwidth limit = Bandwidth.classic(50, Refill.intervally(50, Duration.ofMinutes(1))) .withInitialTokens(25) // Start with 25 tokens .withId("api-limit"); // Identifier for the limit Bucket bucket = Bucket4j.builder() .addLimit(limit) .build(); } }

Spring Boot Integration

Configuration Class

@Configuration public class RateLimitConfig { @Bean public Bucket apiRateLimitBucket() { Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))); return Bucket4j.builder().addLimit(limit).build(); } @Bean public Bucket loginRateLimitBucket() { Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1))); return Bucket4j.builder().addLimit(limit).build(); } @Bean public Bucket premiumUserBucket() { Bandwidth premiumLimit = Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1))); return Bucket4j.builder().addLimit(premiumLimit).build(); } }

Rate Limit Service

@Service public class RateLimitService { private final Bucket apiRateLimitBucket; private final Bucket loginRateLimitBucket; private final Bucket premiumUserBucket; // Cache for user-specific buckets private final Cache<String, Bucket> userBuckets = Caffeine.newBuilder() .expireAfterAccess(Duration.ofHours(1)) .build(); public RateLimitService(Bucket apiRateLimitBucket, Bucket loginRateLimitBucket, Bucket premiumUserBucket) { this.apiRateLimitBucket = apiRateLimitBucket; this.loginRateLimitBucket = loginRateLimitBucket; this.premiumUserBucket = premiumUserBucket; } public boolean tryApiCall() { return apiRateLimitBucket.tryConsume(1); } public boolean tryLogin(String username) { return loginRateLimitBucket.tryConsume(1); } public boolean tryConsume(String bucketKey, long tokens) { Bucket bucket = userBuckets.get(bucketKey, this::createUserBucket); return bucket.tryConsume(tokens); } private Bucket createUserBucket(String key) { Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))); return Bucket4j.builder().addLimit(limit).build(); } public ConsumptionProbe tryConsumeWithDetail(String bucketKey, long tokens) { Bucket bucket = userBuckets.get(bucketKey, this::createUserBucket); return bucket.tryConsumeAndReturnRemaining(tokens); } public Map<String, Object> getRateLimitInfo(String bucketKey) { Bucket bucket = userBuckets.getIfPresent(bucketKey); if (bucket == null) { return Map.of("available", 0, "waitTime", 0); } return Map.of( "available", bucket.getAvailableTokens(), "waitTime", bucket.getAvailableTokens() > 0 ? 0 : bucket.estimateAbilityToConsume(1).getNanosToWait() / 1_000_000_000 ); } }

Spring Web MVC Integration

Rate Limit Annotation

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimited { String value() default "global"; long tokens() default 1; String key() default ""; }

Rate Limit Interceptor

@Component public class RateLimitInterceptor implements HandlerInterceptor { private final RateLimitService rateLimitService; public RateLimitInterceptor(RateLimitService rateLimitService) { this.rateLimitService = rateLimitService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RateLimited rateLimited = handlerMethod.getMethodAnnotation(RateLimited.class); if (rateLimited != null) { String bucketKey = resolveBucketKey(request, rateLimited); long tokens = rateLimited.tokens(); ConsumptionProbe probe = rateLimitService.tryConsumeWithDetail(bucketKey, tokens); if (!probe.isConsumed()) { response.setStatus(429); // Too Many Requests response.setContentType("application/json"); Map<String, Object> error = Map.of( "error", "Rate limit exceeded", "message", "Too many requests", "retryAfter", probe.getNanosToWaitForRefill() / 1_000_000_000, "availableTokens", probe.getRemainingTokens() ); response.getWriter().write(new ObjectMapper().writeValueAsString(error)); return false; } // Add rate limit headers addRateLimitHeaders(response, probe); } } return true; } private String resolveBucketKey(HttpServletRequest request, RateLimited rateLimited) { if (!rateLimited.key().isEmpty()) { return rateLimited.key(); } // Use IP address as default key String ipAddress = getClientIpAddress(request); return rateLimited.value() + ":" + ipAddress; } private String getClientIpAddress(HttpServletRequest request) { String xfHeader = request.getHeader("X-Forwarded-For"); if (xfHeader != null) { return xfHeader.split(",")[0]; } return request.getRemoteAddr(); } private void addRateLimitHeaders(HttpServletResponse response, ConsumptionProbe probe) { response.setHeader("X-RateLimit-Limit", String.valueOf(probe.getConfiguration().getBandwidths()[0].getCapacity())); response.setHeader("X-RateLimit-Remaining", String.valueOf(probe.getRemainingTokens())); response.setHeader("X-RateLimit-Reset", String.valueOf( System.currentTimeMillis() + probe.getNanosToWaitForRefill() / 1_000_000)); } }

Controller Usage

@RestController @RequestMapping("/api") public class ApiController { private final RateLimitService rateLimitService; public ApiController(RateLimitService rateLimitService) { this.rateLimitService = rateLimitService; } @GetMapping("/public/data") @RateLimited(value = "public-api", tokens = 1) public ResponseEntity<?> getPublicData() { return ResponseEntity.ok(Map.of("data", "Public data accessed")); } @PostMapping("/auth/login") @RateLimited(value = "login", tokens = 1) public ResponseEntity<?> login(@RequestBody LoginRequest request) { if (rateLimitService.tryLogin(request.getUsername())) { // Process login return ResponseEntity.ok(Map.of("status", "Login successful")); } else { return ResponseEntity.status(429).body(Map.of("error", "Too many login attempts")); } } @GetMapping("/user/profile") @RateLimited(value = "user-api", key = "#userId", tokens = 1) public ResponseEntity<?> getUserProfile(@RequestParam String userId) { return ResponseEntity.ok(Map.of("profile", "User profile data")); } @GetMapping("/premium/data") @RateLimited(value = "premium-api", tokens = 5) public ResponseEntity<?> getPremiumData() { return ResponseEntity.ok(Map.of("data", "Premium data accessed")); } }

Distributed Rate Limiting

JCache (JSR-107) Integration

@Configuration public class DistributedRateLimitConfig { @Bean public javax.cache.Cache<String, byte[]> cache() { CachingProvider cachingProvider = Caching.getCachingProvider(); CacheManager cacheManager = cachingProvider.getCacheManager(); CompleteConfiguration<String, byte[]> config = new MutableConfiguration<String, byte[]>() .setTypes(String.class, byte.class); return cacheManager.createCache("rate-limit-cache", config); } @Bean public ProxyManager<String> proxyManager(javax.cache.Cache<String, byte[]> cache) { return new JCacheProxyManager<>(cache); } @Bean public Bucket userBucket(ProxyManager<String> proxyManager) { Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))); BucketConfiguration configuration = BucketConfiguration.builder() .addLimit(limit) .build(); return proxyManager.builder().build("user-bucket", configuration); } }

Hazelcast Backend

@Configuration public class HazelcastConfig { @Bean public HazelcastInstance hazelcastInstance() { Config config = new Config(); config.setClusterName("rate-limit-cluster"); // Configure map for rate limiting MapConfig mapConfig = new MapConfig(); mapConfig.setName("rate-limit-buckets"); mapConfig.setTimeToLiveSeconds(3600); config.addMapConfig(mapConfig); return Hazelcast.newHazelcastInstance(config); } @Bean public ProxyManager<String> hazelcastProxyManager(HazelcastInstance hazelcastInstance) { return new HazelcastProxyManager<>(hazelcastInstance); } }

Redis Backend

@Configuration public class RedisRateLimitConfig { @Bean public RedisClient redisClient() { return RedisClient.create("redis://localhost:6379"); } @Bean public ProxyManager<String> redisProxyManager(RedisClient redisClient) { return new RedisProxyManager<>(redisClient); } @Bean public BucketConfiguration globalBucketConfig() { return BucketConfiguration.builder() .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1)))) .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) .build(); } }

Advanced Use Cases

Tiered Rate Limiting

@Service public class TieredRateLimitService { private final Map<String, BucketConfiguration> tierConfigs; private final ProxyManager<String> proxyManager; public TieredRateLimitService(ProxyManager<String> proxyManager) { this.proxyManager = proxyManager; this.tierConfigs = createTierConfigurations(); } private Map<String, BucketConfiguration> createTierConfigurations() { Map<String, BucketConfiguration> configs = new HashMap<>(); // Free tier configs.put("free", BucketConfiguration.builder() .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)))) .build()); // Basic tier configs.put("basic", BucketConfiguration.builder() .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1)))) .build()); // Premium tier configs.put("premium", BucketConfiguration.builder() .addLimit(Bandwidth.classic(10000, Refill.intervally(10000, Duration.ofHours(1)))) .build()); return configs; } public boolean tryConsume(String userId, String tier, long tokens) { BucketConfiguration config = tierConfigs.getOrDefault(tier, tierConfigs.get("free")); Bucket bucket = proxyManager.builder().build(userId, config); return bucket.tryConsume(tokens); } public ConsumptionProbe tryConsumeWithDetail(String userId, String tier, long tokens) { BucketConfiguration config = tierConfigs.getOrDefault(tier, tierConfigs.get("free")); Bucket bucket = proxyManager.builder().build(userId, config); return bucket.tryConsumeAndReturnRemaining(tokens); } }

Adaptive Rate Limiting

@Service public class AdaptiveRateLimitService { private final Map<String, Bucket> dynamicBuckets = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); @PostConstruct public void init() { // Clean up expired buckets every hour scheduler.scheduleAtFixedRate(this::cleanupExpiredBuckets, 1, 1, TimeUnit.HOURS); } public boolean tryConsumeAdaptive(String key, long baseRate, double loadFactor) { Bucket bucket = dynamicBuckets.computeIfAbsent(key, k -> createAdaptiveBucket(baseRate, loadFactor)); return bucket.tryConsume(1); } private Bucket createAdaptiveBucket(long baseRate, double loadFactor) { // Adjust rate based on system load long adjustedRate = (long) (baseRate / loadFactor); adjustedRate = Math.max(adjustedRate, 10); // Minimum rate Bandwidth limit = Bandwidth.classic(adjustedRate, Refill.intervally(adjustedRate, Duration.ofMinutes(1))); return Bucket4j.builder().addLimit(limit).build(); } public void updateRateLimit(String key, long newRate) { dynamicBuckets.computeIfPresent(key, (k, bucket) -> { Bandwidth newLimit = Bandwidth.classic(newRate, Refill.intervally(newRate, Duration.ofMinutes(1))); return Bucket4j.builder().addLimit(newLimit).build(); }); } private void cleanupExpiredBuckets() { long cutoff = System.currentTimeMillis() - Duration.ofHours(24).toMillis(); // Implement cleanup logic based on last access time } @PreDestroy public void shutdown() { scheduler.shutdown(); } }

Monitoring and Metrics

Rate Limit Metrics

@Component public class RateLimitMetrics { private final MeterRegistry meterRegistry; private final Counter allowedRequests; private final Counter limitedRequests; private final DistributionSummary waitTimeSummary; public RateLimitMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.allowedRequests = Counter.builder("rate_limit.requests") .tag("status", "allowed") .register(meterRegistry); this.limitedRequests = Counter.builder("rate_limit.requests") .tag("status", "limited") .register(meterRegistry); this.waitTimeSummary = DistributionSummary.builder("rate_limit.wait_time") .register(meterRegistry); } public void recordRequest(boolean allowed, long waitTimeNanos) { if (allowed) { allowedRequests.increment(); } else { limitedRequests.increment(); } if (waitTimeNanos > 0) { waitTimeSummary.record(waitTimeNanos / 1_000_000.0); // Convert to milliseconds } } public void recordBucketState(String bucketKey, long availableTokens, long capacity) { Gauge.builder("rate_limit.available_tokens", () -> availableTokens) .tag("bucket", bucketKey) .register(meterRegistry); Gauge.builder("rate_limit.capacity", () -> capacity) .tag("bucket", bucketKey) .register(meterRegistry); } }

Rate Limit Manager

@Service public class RateLimitManager { private final RateLimitMetrics metrics; private final ProxyManager<String> proxyManager; private final Map<String, BucketConfiguration> configurations; public RateLimitManager(RateLimitMetrics metrics, ProxyManager<String> proxyManager) { this.metrics = metrics; this.proxyManager = proxyManager; this.configurations = loadConfigurations(); } public RateLimitResult tryConsume(String bucketKey, String configName, long tokens) { BucketConfiguration config = configurations.get(configName); if (config == null) { return new RateLimitResult(true, 0, 0, 0); } Bucket bucket = proxyManager.builder().build(bucketKey, config); ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(tokens); // Record metrics metrics.recordRequest(probe.isConsumed(), probe.getNanosToWaitForRefill()); metrics.recordBucketState(bucketKey, probe.getRemainingTokens(), config.getBandwidths()[0].getCapacity()); return new RateLimitResult( probe.isConsumed(), probe.getRemainingTokens(), probe.getNanosToWaitForRefill(), config.getBandwidths()[0].getCapacity() ); } public void updateConfiguration(String configName, BucketConfiguration newConfig) { configurations.put(configName, newConfig); } public Map<String, Object> getBucketStats(String bucketKey, String configName) { BucketConfiguration config = configurations.get(configName); if (config == null) { return Map.of(); } Bucket bucket = proxyManager.builder().build(bucketKey, config); long availableTokens = bucket.getAvailableTokens(); return Map.of( "availableTokens", availableTokens, "capacity", config.getBandwidths()[0].getCapacity(), "config", configName ); } private Map<String, BucketConfiguration> loadConfigurations() { Map<String, BucketConfiguration> configs = new HashMap<>(); // Load from database or configuration file configs.put("api-global", BucketConfiguration.builder() .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1)))) .build()); configs.put("api-user", BucketConfiguration.builder() .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) .build()); return configs; } public static class RateLimitResult { private final boolean allowed; private final long remainingTokens; private final long waitTimeNanos; private final long capacity; public RateLimitResult(boolean allowed, long remainingTokens, long waitTimeNanos, long capacity) { this.allowed = allowed; this.remainingTokens = remainingTokens; this.waitTimeNanos = waitTimeNanos; this.capacity = capacity; } // Getters public boolean isAllowed() { return allowed; } public long getRemainingTokens() { return remainingTokens; } public long getWaitTimeNanos() { return waitTimeNanos; } public long getCapacity() { return capacity; } } }

Testing Rate Limiting

Unit Tests

@ExtendWith(MockitoExtension.class) class RateLimitServiceTest { @InjectMocks private RateLimitService rateLimitService; private Bucket bucket; @BeforeEach void setUp() { Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1))); bucket = Bucket4j.builder().addLimit(limit).build(); } @Test void testRateLimitAllowsWithinLimit() { for (int i = 0; i < 5; i++) { assertTrue(bucket.tryConsume(1), "Request " + (i + 1) + " should be allowed"); } } @Test void testRateLimitBlocksBeyondLimit() { // Consume all tokens for (int i = 0; i < 5; i++) { bucket.tryConsume(1); } // Next request should be blocked assertFalse(bucket.tryConsume(1), "Request beyond limit should be blocked"); } @Test void testConsumptionProbe() { ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1); assertTrue(probe.isConsumed()); assertEquals(4, probe.getRemainingTokens()); } } @SpringBootTest class RateLimitIntegrationTest { @Autowired private RateLimitService rateLimitService; @Test void testDistributedRateLimiting() { String user1 = "user1"; String user2 = "user2"; // User1 consumes tokens for (int i = 0; i < 5; i++) { assertTrue(rateLimitService.tryConsume(user1, 1)); } // User1 should be limited assertFalse(rateLimitService.tryConsume(user1, 1)); // User2 should still have tokens assertTrue(rateLimitService.tryConsume(user2, 1)); } }

Best Practices

Production Configuration

@Configuration public class ProductionRateLimitConfig { @Bean @Primary public ProxyManager<String> proxyManager(RedisClient redisClient) { return new RedisProxyManager<>(redisClient); } @Bean public BucketConfiguration apiGlobalConfig() { return BucketConfiguration.builder() .addLimit(Bandwidth.classic(10000, Refill.intervally(10000, Duration.ofHours(1)))) .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofMinutes(1)))) .build(); } @Bean public BucketConfiguration apiUserConfig() { return BucketConfiguration.builder() .addLimit(Bandwidth.classic(1000, Refill.intervally(1000, Duration.ofHours(1)))) .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) .build(); } @Bean public BucketConfiguration loginConfig() { return BucketConfiguration.builder() .addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1)))) .build(); } } // Configuration properties @ConfigurationProperties(prefix = "rate-limiting") @Data public class RateLimitProperties { private Map<String, RateLimitConfig> configurations = new HashMap<>(); @Data public static class RateLimitConfig { private long capacity; private Duration period; private long initialTokens; } }

Conclusion

Bucket4j provides a robust and flexible rate-limiting solution for Java applications. Key benefits include:

  • Multiple algorithms: Token bucket, leaky bucket support
  • Distributed support: Redis, Hazelcast, JCache backends
  • Flexible configuration: Multiple bandwidth limits, custom refill strategies
  • Spring integration: Easy integration with Spring Boot applications
  • Monitoring: Comprehensive metrics and monitoring support
  • Performance: High-performance implementation suitable for production use

By implementing rate limiting with Bucket4j, you can protect your APIs from abuse, ensure fair usage, and maintain system stability under high load conditions.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper