Java API Rate Limiting Strategies

Loading

Rate limiting is a technique used to control the number of requests a client can make to an API within a specific time period. It helps prevent abuse, ensure fair usage, and maintain system stability. Below are strategies and examples for implementing rate limiting in Java APIs.


1. Token Bucket Algorithm

The Token Bucket algorithm is a popular rate-limiting technique. It uses a bucket to hold tokens, where each request consumes a token. Tokens are refilled at a fixed rate.

Implementation:

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class TokenBucketRateLimiter {
    private final Semaphore tokens;
    private final int capacity;
    private final long refillPeriod;
    private final TimeUnit timeUnit;

    public TokenBucketRateLimiter(int capacity, long refillPeriod, TimeUnit timeUnit) {
        this.capacity = capacity;
        this.tokens = new Semaphore(capacity);
        this.refillPeriod = refillPeriod;
        this.timeUnit = timeUnit;
        startRefillTask();
    }

    public boolean tryAcquire() {
        return tokens.tryAcquire();
    }

    private void startRefillTask() {
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(timeUnit.toMillis(refillPeriod));
                    tokens.release(capacity - tokens.availablePermits());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();
    }

    public static void main(String[] args) {
        TokenBucketRateLimiter rateLimiter = new TokenBucketRateLimiter(10, 1, TimeUnit.SECONDS);

        for (int i = 0; i < 15; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + (i + 1) + " allowed");
            } else {
                System.out.println("Request " + (i + 1) + " denied");
            }
        }
    }
}

2. Fixed Window Counter

The Fixed Window Counter algorithm divides time into fixed windows (e.g., 1 minute) and limits the number of requests per window.

Implementation:

import java.util.concurrent.atomic.AtomicInteger;

public class FixedWindowRateLimiter {
    private final int limit;
    private final long windowSizeInMillis;
    private AtomicInteger counter;
    private long windowStart;

    public FixedWindowRateLimiter(int limit, long windowSizeInMillis) {
        this.limit = limit;
        this.windowSizeInMillis = windowSizeInMillis;
        this.counter = new AtomicInteger(0);
        this.windowStart = System.currentTimeMillis();
    }

    public boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - windowStart > windowSizeInMillis) {
            counter.set(0);
            windowStart = currentTime;
        }
        return counter.incrementAndGet() <= limit;
    }

    public static void main(String[] args) throws InterruptedException {
        FixedWindowRateLimiter rateLimiter = new FixedWindowRateLimiter(5, 1000);

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + (i + 1) + " allowed");
            } else {
                System.out.println("Request " + (i + 1) + " denied");
            }
            Thread.sleep(200);
        }
    }
}

3. Sliding Window Log

The Sliding Window Log algorithm maintains a log of timestamps for each request and limits requests based on a sliding time window.

Implementation:

import java.util.LinkedList;
import java.util.Queue;

public class SlidingWindowRateLimiter {
    private final int limit;
    private final long windowSizeInMillis;
    private final Queue<Long> log;

    public SlidingWindowRateLimiter(int limit, long windowSizeInMillis) {
        this.limit = limit;
        this.windowSizeInMillis = windowSizeInMillis;
        this.log = new LinkedList<>();
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        while (!log.isEmpty() && currentTime - log.peek() > windowSizeInMillis) {
            log.poll();
        }
        if (log.size() < limit) {
            log.offer(currentTime);
            return true;
        }
        return false;
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowRateLimiter rateLimiter = new SlidingWindowRateLimiter(5, 1000);

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + (i + 1) + " allowed");
            } else {
                System.out.println("Request " + (i + 1) + " denied");
            }
            Thread.sleep(200);
        }
    }
}

4. Distributed Rate Limiting

For distributed systems, use a centralized rate-limiting service like Redis to synchronize rate limits across multiple instances.

Implementation with Redis:

import redis.clients.jedis.Jedis;

public class RedisRateLimiter {
    private final Jedis jedis;
    private final String key;
    private final int limit;
    private final long windowSizeInMillis;

    public RedisRateLimiter(Jedis jedis, String key, int limit, long windowSizeInMillis) {
        this.jedis = jedis;
        this.key = key;
        this.limit = limit;
        this.windowSizeInMillis = windowSizeInMillis;
    }

    public boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - windowSizeInMillis;

        jedis.zremrangeByScore(key, 0, windowStart);
        long count = jedis.zcard(key);

        if (count < limit) {
            jedis.zadd(key, currentTime, String.valueOf(currentTime));
            jedis.expire(key, windowSizeInMillis / 1000);
            return true;
        }
        return false;
    }

    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis("localhost");
        RedisRateLimiter rateLimiter = new RedisRateLimiter(jedis, "api-rate-limit", 5, 1000);

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request " + (i + 1) + " allowed");
            } else {
                System.out.println("Request " + (i + 1) + " denied");
            }
            Thread.sleep(200);
        }

        jedis.close();
    }
}

5. Using Rate Limiting Libraries

Libraries like Resilience4j and Guava provide built-in rate-limiting capabilities.

Resilience4j Example:

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;

import java.time.Duration;

public class Resilience4jRateLimiter {
    public static void main(String[] args) {
        RateLimiterConfig config = RateLimiterConfig.custom()
            .limitForPeriod(5)
            .limitRefreshPeriod(Duration.ofSeconds(1))
            .timeoutDuration(Duration.ofMillis(100))
            .build();

        RateLimiterRegistry registry = RateLimiterRegistry.of(config);
        RateLimiter rateLimiter = registry.rateLimiter("my-rate-limiter");

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.acquirePermission()) {
                System.out.println("Request " + (i + 1) + " allowed");
            } else {
                System.out.println("Request " + (i + 1) + " denied");
            }
        }
    }
}

6. Best Practices

  • Monitor Rate Limits: Track rate limit usage and adjust limits as needed.
  • Graceful Degradation: Return meaningful error messages (e.g., 429 Too Many Requests) when limits are exceeded.
  • Distributed Systems: Use centralized rate-limiting mechanisms (e.g., Redis) for consistency.
  • Caching: Cache rate limit data to reduce latency.

By implementing these strategies, you can effectively control API usage, prevent abuse, and ensure the stability of your Java applications.

Leave a Reply

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