Skip to content

Spring Boot Rate Limiting — API Throttling with Bucket4j

DodaTech Updated 2026-06-28 4 min read

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

  1. What rate limiting algorithm does Bucket4j implement?
  2. How do you cache buckets per API key?
  3. Why is Redis needed in multi-instance deployments?
  4. What is greedy refill in Bucket4j?
  5. How do you add rate limit headers to responses?

Answers

  1. 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

What is Bucket4j?

A Java library implementing the token-bucket rate limiting algorithm with support for distributed backends.

Does Bucket4j support distributed rate limiting?

Yes. Use ProxyManager with Redis, Hazelcast, or other distributed stores.

How does Bucket4j handle token refill?

Greedy refill adds tokens continuously. Intervally refill adds all tokens at fixed intervals.

Can Bucket4j be used with Spring Boot interceptors?

Yes. Implement HandlerInterceptor and check bucket.tryConsume() in preHandle().

What is a ProxyManager in Bucket4j?

A distributed bucket manager that stores and retrieves bucket state from a remote store like Redis.

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