Class RateLimiter

java.lang.Object
org.omnifaces.cdi.ratelimit.RateLimiter

@ApplicationScoped public class RateLimiter extends Object

CDI managed bean for rate limiting requests per client identifier. This utility provides a thread-safe fixed window rate limiting mechanism with configurable request limits and time windows. Callers with the same client identifier, maximum number of requests and time window share one counter.

Both public checkRateLimit methods share one counter namespace: checkRateLimit(String, int, Duration, int) with a client IP address as client identifier shares its counter with checkRateLimit(HttpServletRequest, int, Duration, int) for a request from that client IP address, given the same limits. The RateLimit annotation without clientId does not share counters with either.

The rate limiter supports both HTTP request-based rate limiting (using client IP addresses) and custom client identifier-based rate limiting (using any string identifier such as user IDs, API keys, etc.). It uses a ConcurrentHashMap to track request counts per client identifier and automatically cleans up expired entries to prevent memory leaks.

When the rate limit is exceeded, the rate limiter will by default immediately throw a RateLimitExceededException. Optionally, you can configure automatic retries via the maxRetries parameter, which will retry the request after a calculated delay based on the remaining time window. If all retries are exhausted, a RateLimitExceededException is thrown.

The number of tracked request counters is capped by the "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS" context parameter, which defaults to 100000. There is one counter per client identifier per limit, and, for the RateLimit annotation without clientId, per annotated method or class. When the cap is reached and expired counters cannot be removed, which is attempted at most once per second, a request which needs a new counter is rejected with a RateLimitExceededException, while existing counters continue to be checked as usual.

A request which needs a new counter is rejected rather than let through, so that filling the cap cannot be used to bypass the rate limit. The trade-off is that a client which can choose many client identifiers can fill the cap and lock out new clients for the duration of the time window. When the client identifier is derived from user input, such as a user name submitted to a login form, then validate it before passing it to checkRateLimit(String, int, Duration, int).

Usage

The recommended usage is with the RateLimit annotation.

 @Named
 @RequestScoped
 public class ApiController {

     @RateLimit(clientId = "FooAPI", maxRequestsPerTimeWindow = 10, maxRetries = 3)
     public void processFooApiRequest() {
         // Process Foo API request ...
     }

 }
 

For more fine grained usage, or when you have a variable rate limit parameter which therefore cannot be set as an annotation attribute, then you can inject this CDI bean in any CDI managed artifact and explicitly invoke either checkRateLimit(HttpServletRequest, int, Duration, int) or checkRateLimit(String, int, Duration, int). Below is an example of a servlet filter that applies rate limiting to all requests depending on client IP address:

 @WebFilter("/*")
 public class RateLimitFilter extends HttpFilter {

     @Inject
     private RateLimiter rateLimiter;

     @Override
     public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
         var maxRequestsPerTimeWindow = determineMaxRequestsBasedOn(request);
         var timeWindowInSeconds = determineTimeWindowBasedOn(request);
         var maxRetries = 0;

         try {
             rateLimiter.checkRateLimit(request, maxRequestsPerTimeWindow, Duration.ofSeconds(timeWindowInSeconds), maxRetries);
             chain.doFilter(request, response);
         }
         catch (RateLimitExceededException e) {
             response.sendError(429); // Too Many Requests
         }
     }

 }
 
Since:
5.0
Author:
Bauke Scholtz
See Also:
  • Field Details

    • PARAM_NAME_MAX_TRACKED_COUNTERS

      public static final String PARAM_NAME_MAX_TRACKED_COUNTERS
      The context parameter name to specify the maximum number of request counters to track: "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS".
      Since:
      5.5.7
      See Also:
    • DEFAULT_MAX_TRACKED_COUNTERS

      public static final int DEFAULT_MAX_TRACKED_COUNTERS
      The default maximum number of request counters to track: 100000.
      Since:
      5.5.7
      See Also:
  • Constructor Details

    • RateLimiter

      public RateLimiter()
  • Method Details

    • init

      @PostConstruct public void init()
      First reads the "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS" context parameter, if any. Then looks up the JEE default managed scheduled executor service in JNDI. If it isn't available, then create scheduled executor service ourselves with help of Executors.newSingleThreadScheduledExecutor(ThreadFactory). Finally schedule the periodic removal of expired request counters on it.
      Throws:
      IllegalArgumentException - When the "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS" context parameter is not a positive number.
    • checkRateLimit

      public void checkRateLimit(HttpServletRequest request, int maxRequestsPerTimeWindow, Duration timeWindow, int maxRetries) throws RateLimitExceededException
      Checks if the current request exceeds the configured rate limit for the client IP address associated with the given request.

      The client IP address is obtained by ServletRequest.getRemoteAddr(). Proxy headers such as X-Forwarded-For are not consulted, because the client controls them. When the application runs behind a reverse proxy, configure the servlet container to resolve the client IP address from the proxy headers of trusted proxies only, such as Tomcat's RemoteIpValve. Otherwise all clients share the IP address of the proxy.

      An IPv6 address is tracked by its /64 network prefix, because a single client usually controls a whole /64 network. An IPv4-mapped IPv6 address, or one in the NAT64 well-known prefix 64:ff9b::/96, is tracked as its embedded IPv4 address.

      This method implements a fixed window rate limiting algorithm: the time window of a counter starts at its first request, and the counter resets when the time window has expired. It tracks the number of requests per IP address within the specified time window and throws an exception if the limit is exceeded. Expired tracking entries are automatically cleaned up to prevent memory leaks.

      Parameters:
      request - The HTTP servlet request to check.
      maxRequestsPerTimeWindow - The maximum number of requests allowed within the time window.
      timeWindow - The time window duration.
      maxRetries - The maximum number of retries.
      Throws:
      RateLimitExceededException - When the rate limit is exceeded for the client IP address associated with the given request.
    • checkRateLimit

      public void checkRateLimit(String clientId, int maxRequestsPerTimeWindow, Duration timeWindow, int maxRetries) throws RateLimitExceededException
      Checks if the current request exceeds the configured rate limit for the given client identifier.

      This method implements a fixed window rate limiting algorithm: the time window of a counter starts at its first request, and the counter resets when the time window has expired. It tracks the number of requests per client identifier within the specified time window and throws an exception if the limit is exceeded. Expired tracking entries are automatically cleaned up to prevent memory leaks.

      When the number of tracked request counters has reached the cap set by the "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS" context parameter, a request which needs a new counter is rejected immediately, without retries.

      Parameters:
      clientId - The client identifier to check, whether client IP, user ID, API key, etc.
      maxRequestsPerTimeWindow - The maximum number of requests allowed within the time window.
      timeWindow - The time window duration.
      maxRetries - The maximum number of retries.
      Throws:
      RateLimitExceededException - When the rate limit is exceeded for the given client identifier.
    • destroy

      @PreDestroy public void destroy()
      Cancels the periodic removal of expired request counters. If the scheduled executor service was created with help of Executors.newSingleThreadScheduledExecutor(ThreadFactory), then attempt to orderly shut down it. If it's still not shut down after 5 seconds, then terminate it.