Skip to main content
resilience patterns in production

Rate Limiting Strategies for Different Traffic Patterns

3 min read Chapter 16 of 40

Rate Limiting Strategies for Different Traffic Patterns

Resilience4J’s RateLimiter uses a fixed window (refresh period) by default. This has a known boundary problem that matters for the transaction platform.

The Fixed Window Boundary Problem

With limit-for-period: 100 and limit-refresh-period: 1s, the limiter allows 100 calls in each 1-second window. If 100 calls arrive in the last 10ms of one window and 100 calls arrive in the first 10ms of the next window, 200 calls pass through in a 20ms span. The effective burst rate is 10,000/second for that 20ms window, not 100/second.

For the SMS provider with a strict 100/second limit, this boundary burst could trigger rate limiting on their side. The provider’s rate limiter may use a sliding window and see 200 requests in any 1-second sliding window.

Sliding Window with AtomicStampedReference

// FROM SCRATCH - Sliding window rate limiter
public class SlidingWindowRateLimiter {

    private final int windowSizeMs;
    private final int maxRequests;
    private final ConcurrentLinkedDeque<Long> timestamps = new ConcurrentLinkedDeque<>();

    public SlidingWindowRateLimiter(int maxRequestsPerSecond) {
        this.windowSizeMs = 1000;
        this.maxRequests = maxRequestsPerSecond;
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long windowStart = now - windowSizeMs;

        // Remove expired entries
        while (!timestamps.isEmpty() && timestamps.peekFirst() < windowStart) {
            timestamps.pollFirst();
        }

        if (timestamps.size() < maxRequests) {
            timestamps.addLast(now);
            return true;
        }

        return false;
    }
}

This sliding window counts requests in a true 1-second sliding window. The boundary problem is eliminated. The tradeoff is higher memory usage (storing a timestamp per request) and contention on the deque under high concurrency.

For the transaction platform, the Resilience4J fixed window with a timeout-duration of 500ms is sufficient. The SMS provider’s rate limiter is lenient enough to tolerate boundary bursts, and the timeout smooths the request rate. If the provider’s rate limiter is strict (exact sliding window enforcement), use the Redis-based distributed limiter from the previous section, which uses the token bucket algorithm and does not have boundary problems.

Choosing the Right Strategy

Token bucket (Resilience4J default, Redis implementation): Best for protecting downstream services. Allows bursts up to the bucket capacity. Smooths traffic naturally.

Fixed window: Best when exact per-period accounting is needed. Has boundary problems. Use when the downstream service uses the same fixed window.

Sliding window: Most accurate representation of “X requests per Y seconds.” Higher implementation cost. Use when the downstream service enforces strict per-second limits.

For every dependency in the transaction platform:

  • Payment gateway: Token bucket. The gateway handles bursts well but has a sustained rate limit.
  • SMS provider: Token bucket via Redis. Strict rate limit that must be enforced across all instances.
  • Audit log: No rate limiter needed. The audit service is internal and scales with the platform.