Rate limiting
Rate limiting caps how often a client may hit a route — login endpoints, public APIs, expensive reports. Kona’s RateLimiter tracks hits in memory by key; ThrottleRequests middleware turns a named limiter into HTTP responses with standard headers.
Artifact: org.konaframework:kona-auth. AuthServiceProvider registers a shared RateLimiter.
Simplest path
Section titled “Simplest path”import org.konaframework.http.HttpKernelimport org.konaframework.http.Routerimport org.konaframework.security.RateLimitimport org.konaframework.security.RateLimiterimport org.konaframework.security.ThrottleRequests
val limiter = app.resolve(RateLimiter::class.java)limiter.forName("login") { request -> RateLimit.perMinute(5) // or RateLimit(5, 60)}
val router = app.resolve(Router::class.java)router.routes { group(prefix = "/auth", middleware = listOf(ThrottleRequests(limiter, "login"))) { post("/login") { /* … */ } }}import org.konaframework.security.RateLimit;import org.konaframework.security.RateLimiter;import org.konaframework.security.ThrottleRequests;import java.util.List;
RateLimiter limiter = app.resolve(RateLimiter.class);limiter.forName("login", request -> RateLimit.perMinute(5));
Router router = app.resolve(Router.class);router.prefix("/auth") .middleware(List.of(new ThrottleRequests(limiter, "login"))) .group(auth -> { auth.post("/login", request -> /* … */); });When the limit is exceeded, the middleware throws RateLimitExceededException — HTTP 429 with a Retry-After header (seconds until the window resets).
Successful responses gain:
X-RateLimit-Limit— max attempts in the windowX-RateLimit-Remaining— attempts left after this hit
Naming and keys
Section titled “Naming and keys”Register limiters with for / forName (Java-friendly alias). Each registration is a callback (Request) -> RateLimit so limits can depend on path, user, or plan:
limiter.forName("api") { request -> val userId = request.getAttribute("user")?.toString() if (userId != null) RateLimit(120, 60) else RateLimit(30, 60)}limiter.forName("api", request -> { Object user = request.getAttribute("user"); return user != null ? new RateLimit(120, 60) : new RateLimit(30, 60);});ThrottleRequests builds the hit key as {limiterName}:{clientKey}. The default client key is:
X-Forwarded-For- else
X-Real-IP - else request attribute
"ip" - else
"global"
Override with a custom resolver:
ThrottleRequests(limiter, "login") { req -> req.input("email") ?: req.header("X-Forwarded-For") ?: "unknown"}new ThrottleRequests(limiter, "login", req -> { String email = req.input("email", null); if (email != null) return email; String ip = req.header("X-Forwarded-For"); return ip != null ? ip : "unknown";});Using the limiter without middleware
Section titled “Using the limiter without middleware”val key = "reports:user-42"if (!limiter.attempt(key, maxAttempts = 10, decaySeconds = 60)) { val wait = limiter.availableIn(key) throw RateLimitExceededException(wait)}// work…limiter.clear(key) // reset after success, if desiredString key = "reports:user-42";if (!limiter.attempt(key, 10, 60)) { throw new RateLimitExceededException(limiter.availableIn(key));}limiter.clear(key);Other helpers: hit, tooManyAttempts, retriesLeft, clearAll.
Failure modes
Section titled “Failure modes”| Situation | Result |
|---|---|
| Unknown limiter name in middleware | IllegalArgumentException |
| Too many attempts | RateLimitExceededException (429 + Retry-After) |
| Process restart | In-memory counters reset (fine for single-node; shared stores land with later infrastructure milestones) |
Place throttle middleware outside expensive handlers but inside identity middleware when the key depends on the authenticated user.