Understanding Interface Idempotency and Distributed Rate Limiting with Token Bucket, Leaky Bucket, Guava RateLimiter, Nginx, and Redis+Lua

This article explains the concept of interface idempotency, demonstrates how to achieve idempotent update operations using version control and token mechanisms, and provides a comprehensive guide to distributed rate limiting—including time‑window and resource‑based dimensions, token‑bucket and leaky‑bucket algorithms, and practical implementations with Guava RateLimiter, Nginx, and Redis‑Lua scripts.

Top Architect
Top Architect
Top Architect
Understanding Interface Idempotency and Distributed Rate Limiting with Token Bucket, Leaky Bucket, Guava RateLimiter, Nginx, and Redis+Lua

1. Interface Idempotency

Interface idempotency means that multiple identical requests produce the same result without side effects; a typical example is a payment request that could be retried due to network failure, potentially causing double charging.

The core idea is to guarantee idempotency by using a unique business identifier and, in concurrent scenarios, applying a lock during the operation.

1) Idempotent Update Operations

1) Update based on a unique business identifier

Version‑based optimistic locking can ensure idempotent updates: the client reads the current version, submits the new data together with the version, and the backend updates only when the version matches.

update set version = version + 1, xxx=${xxx} where id = xxx and version = ${version};

2) Token mechanism for update/insert idempotency

1) Operations without a unique business identifier

When a user accesses the registration page, the backend generates a token and returns it in a hidden field. The token is sent back on submission, used to acquire a distributed lock, and the insert operation proceeds only if the lock is held; the lock is released automatically after expiration.

2. Distributed Rate Limiting

1) Dimensions of distributed rate limiting

Time‑based limiting uses a time window (e.g., per second, per minute). Resource‑based limiting caps the number of accesses or concurrent connections.

In real scenarios, multiple rules are combined, such as limiting each IP to 10 requests per second, limiting connections per server, and applying higher‑level limits for a whole server group.

2) Common rate‑limiting algorithms

1) Token Bucket Algorithm

The token bucket algorithm has two key components: a bucket that stores tokens and a token generator that adds tokens at a fixed rate. A request can proceed only when it successfully acquires a token; otherwise it is queued or rejected.

Token Bucket Diagram
Token Bucket Diagram

Token generation is performed at a steady rate (e.g., 100 tokens per second). If the bucket is full, new tokens are discarded.

When a request arrives, it must obtain a token. If tokens are exhausted, the request can be placed in a buffer queue (optional) or rejected.

2) Leaky Bucket Algorithm

The leaky bucket algorithm is similar to the token bucket but operates on request packets instead of tokens. Packets are added to the bucket; if the bucket is full, excess packets are dropped.

The bucket drains at a constant rate, ensuring a steady output flow regardless of bursty input.

3) Main distributed rate‑limiting solutions

1) Guava RateLimiter client‑side limiting

Add the Guava dependency:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

Example controller:

@RestController
@Slf4j
public class Controller {
    // 2 tokens per second
    RateLimiter limiter = RateLimiter.create(2.0);

    // Non‑blocking limit
    @GetMapping("/tryAcquire")
    public String tryAcquire(Integer count) {
        if (limiter.tryAcquire(count)) {
            log.info("Success, rate = {}", limiter.getRate());
            return "success";
        } else {
            log.info("Rejected, rate = {}", limiter.getRate());
            return "fail";
        }
    }

    // Blocking limit with timeout
    @GetMapping("/tryAcquireWithTimeout")
    public String tryAcquireWithTimeout(Integer count, Integer timeout) {
        if (limiter.tryAcquire(count, timeout, TimeUnit.SECONDS)) {
            log.info("Success, rate = {}", limiter.getRate());
            return "success";
        } else {
            log.info("Rejected, rate = {}", limiter.getRate());
            return "fail";
        }
    }

    // Synchronous blocking limit
    @GetMapping("/acquire")
    public String acquire(Integer count) {
        limiter.acquire(count);
        log.info("Success, rate = {}", limiter.getRate());
        return "success";
    }
}

2) Nginx rate limiting

Configure host mapping: 127.0.0.1 www.test.com Edit /usr/local/nginx/conf/nginx.conf and add a limit zone:

# Define a memory zone for IP‑based limiting
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;

server {
    server_name www.test.com;
    location /access-limit/ {
        proxy_pass http://127.0.0.1:8080/;
        # Apply the limit with a burst of 2 and no delay
        limit_req zone=iplimit burst=2 nodelay;
    }
}

3) Redis + Lua distributed limiting

Lua script (saved as rateLimiter.lua):

-- Get the method key
local methodKey = KEYS[1]
local limit = tonumber(ARGV[1])
local count = tonumber(redis.call('get', methodKey) or "0")
if count + 1 > limit then
    return false
else
    redis.call('INCRBY', methodKey, 1)
    redis.call('EXPIRE', methodKey, 1)
    return true
end

Spring configuration to load the script:

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }

    @Bean
    public DefaultRedisScript<Boolean> loadRedisScript() {
        DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("rateLimiter.lua"));
        script.setResultType(Boolean.class);
        return script;
    }
}

Rate‑limiting service using the script:

@Service
@Slf4j
public class AccessLimiter {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisScript<Boolean> rateLimitLua;

    public void limitAccess(String key, Integer limit) {
        Boolean allowed = stringRedisTemplate.execute(rateLimitLua, Collections.singletonList(key), limit.toString());
        if (!allowed) {
            log.error("Your access is blocked, key={}", key);
            throw new RuntimeException("Your access is blocked");
        }
    }
}

Controller that uses the limiter:

@RestController
@Slf4j
public class Controller {
    @Autowired
    private AccessLimiter accessLimiter;

    @GetMapping("/test")
    public String test() {
        accessLimiter.limitAccess("ratelimiter-test", 1);
        return "success";
    }
}

4) Custom annotation and AOP for automatic limiting

Define annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiterAop {
    int limit();
    String methodKey() default "";
}

Aspect that applies the limiter before method execution:

@Aspect
@Component
@Slf4j
public class AccessLimiterAspect {
    @Autowired
    private AccessLimiter accessLimiter;

    @Pointcut("@annotation(com.example.AccessLimiterAop)")
    public void cut() {}

    @Before("cut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        AccessLimiterAop annotation = method.getAnnotation(AccessLimiterAop.class);
        if (annotation == null) return;
        String key = annotation.methodKey();
        int limit = annotation.limit();
        if (StringUtils.isEmpty(key)) {
            key = method.getName();
            Class<?>[] types = method.getParameterTypes();
            if (types != null) {
                String params = Arrays.stream(types).map(Class::getName).collect(Collectors.joining(","));
                key += "#" + params;
            }
        }
        accessLimiter.limitAccess(key, limit);
    }
}

Usage in a controller:

@RestController
@Slf4j
public class Controller {
    @Autowired
    private AccessLimiter accessLimiter;

    @GetMapping("/test")
    @AccessLimiterAop(limit = 1)
    public String test() {
        return "success";
    }
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDistributed SystemsIdempotencyrate limiting
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.