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 Summary
FieldsModifier and TypeFieldDescriptionstatic final intThe default maximum number of request counters to track: 100000.static final StringThe context parameter name to specify the maximum number of request counters to track: "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS". -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidcheckRateLimit(HttpServletRequest request, int maxRequestsPerTimeWindow, Duration timeWindow, int maxRetries) Checks if the current request exceeds the configured rate limit for the client IP address associated with the given request.voidcheckRateLimit(String clientId, int maxRequestsPerTimeWindow, Duration timeWindow, int maxRetries) Checks if the current request exceeds the configured rate limit for the given client identifier.voiddestroy()Cancels the periodic removal of expired request counters.voidinit()First reads the "org.omnifaces.RATE_LIMITER_MAX_TRACKED_COUNTERS" context parameter, if any.
-
Field Details
-
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_COUNTERSThe default maximum number of request counters to track: 100000.- Since:
- 5.5.7
- See Also:
-
-
Constructor Details
-
RateLimiter
public RateLimiter()
-
-
Method Details
-
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 ofExecutors.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 asX-Forwarded-Forare 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'sRemoteIpValve. Otherwise all clients share the IP address of the proxy.An IPv6 address is tracked by its
/64network prefix, because a single client usually controls a whole/64network. An IPv4-mapped IPv6 address, or one in the NAT64 well-known prefix64: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
Cancels the periodic removal of expired request counters. If the scheduled executor service was created with help ofExecutors.newSingleThreadScheduledExecutor(ThreadFactory), then attempt to orderly shut down it. If it's still not shut down after 5 seconds, then terminate it.
-