Ktor 3.6.0 Help

Rate limiting

The RateLimit plugin allows you to limit the number of requests a client can make within a specified time period.

Ktor provides several ways to configure rate limiting:

  • Apply a rate limit globally to the entire application or configure different limits for specific resources.

  • Apply rate limits based on request parameters, such as an IP address, API key or access token.

Add dependencies

To use RateLimit, add the ktor-server-rate-limit artifact to your build script:

implementation("io.ktor:ktor-server-rate-limit:$ktor_version")
implementation "io.ktor:ktor-server-rate-limit:$ktor_version"
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-rate-limit-jvm</artifactId> <version>${ktor_version}</version> </dependency>

Install RateLimit

To install the RateLimit plugin to your application, pass it to the install function in the specified module. The following examples show how to install RateLimit:

  • In an embeddedServer() function call.

  • In an explicitly defined module() extension function on the Application class.

import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.application.* import io.ktor.server.plugins.ratelimit.* fun main() { embeddedServer(Netty, port = 8080) { install(RateLimit) // ... }.start(wait = true) }
import io.ktor.server.application.* import io.ktor.server.plugins.ratelimit.* // ... fun Application.module() { install(RateLimit) // ... }

Configure RateLimit

Overview

Ktor uses the token bucket algorithm for rate limiting, which works as follows:

  1. A bucket is created with a specified capacity, which defines the number of available tokens.

  2. Each incoming request consumes one token from the bucket:

    • If there is enough capacity, the server processes the request and includes the following headers in the response:

      • X-RateLimit-Limit: the bucket capacity.

      • X-RateLimit-Remaining: the number of tokens remaining in the bucket.

      • X-RateLimit-Reset: the UTC timestamp, in seconds, that specifies when the bucket is refilled.

    • If there is insufficient capacity, the server rejects a request using a 429 Too Many Requests response. The response includes the Retry-After header, indicating how many seconds the client should wait before sending another request.

  3. After the specified refill period, the bucket is refilled.

Register a rate limiter

You can apply rate limiting globally to the entire application or register a rate limiter for specific routes:

  • To apply rate limiting globally, call the global() function and configure the rate limiter:

    install(RateLimit) { global { rateLimiter(limit = 5, refillPeriod = 60.seconds) } }
  • To configure rate limiting for specific routes, use the register() function to register a rate limiter:

    install(RateLimit) { register { rateLimiter(limit = 5, refillPeriod = 60.seconds) } }

The examples above show the minimal configuration required for the RateLimit plugin. If you use register(), you also need to apply the registered rate limiter to a specific route.

Configure rate limiting

You can configure a rate limiter using the options below.

Name a rate limiter

Use the register() function to assign a name to a rate limiter. You can then apply the named rate limiter to specific routes:

install(RateLimit) { register(RateLimitName("protected")) { // ... } }

Set the limit and refill period

Use the rateLimiter() function to configure the bucket capacity and refill period:

  • limit specifies the number of available tokens.

  • refillPeriod specifies how often the bucket is refilled.

The following example allows up to 30 requests per minute:

register(RateLimitName("protected")) { rateLimiter(limit = 30, refillPeriod = 60.seconds) }

Distinguish requests by key

Use the requestKey() function to return a key for each request. Requests with different keys have independent rate limits.

The following example uses the login query parameter to distinguish between users:

register(RateLimitName("protected")) { requestKey { applicationCall -> applicationCall.request.queryParameters["login"]!! } }

Rate limit authenticated users

You can use an authentication principal as a request key to apply rate limits per authenticated user.

Nest rateLimit() inside authenticate(), then access the principal from requestKey():

install(Authentication) { basic("auth") { validate { UserIdPrincipal(it.name) } } } install(RateLimit) { register(RateLimitName("per-user")) { rateLimiter(limit = 10, refillPeriod = 60.seconds) requestKey { call.principal<UserIdPrincipal>()?.name ?: "anonymous" } } } routing { authenticate("auth") { rateLimit(RateLimitName("per-user")) { get("/api") { call.respondText("OK") } } } }

Set the request weight

Use the requestWeight() function to specify how many tokens each request consumes. The function receives the application call and the request key.

In the following example, requests with the jetbrains key consume one token, while all other requests consume two:

register(RateLimitName("protected")) { requestKey { applicationCall -> applicationCall.request.queryParameters["login"]!! } requestWeight { applicationCall, key -> when(key) { "jetbrains" -> 1 else -> 2 } } }

Customize the response

Use the modifyResponse() function to customize the response when rate limiting is applied.

For example, you can add custom rate-limit headers:

register(RateLimitName("protected")) { modifyResponse { applicationCall, state -> applicationCall.response.header("X-RateLimit-Custom-Header", "Some value") } }

Define rate limiting scope

After configuring a rate limiter, you can use the rateLimit() function to apply it to specific routes.

Apply the default rate limiter

Use the rateLimit() function without a name to apply the default registered rate limiter:

routing { rateLimit { get("/") { val requestsLeft = call.response.headers["X-RateLimit-Remaining"] call.respondText("Welcome to the home page! $requestsLeft requests left.") } } }

Apply a named rate limiter

Pass a RateLimitName to the rateLimit() function to apply a named rate limiter:

routing { rateLimit(RateLimitName("protected")) { get("/protected-api") { val requestsLeft = call.response.headers["X-RateLimit-Remaining"] val login = call.request.queryParameters["login"] call.respondText("Welcome to protected API, $login! $requestsLeft requests left.") } } }

Example

The following example shows how to apply different rate limiters to different routes. It configures:

  • A default rate limiter for the home page.

  • A named public rate limiter for the public API.

  • A named protected rate limiter that uses request keys and weights.

  • The StatusPages plugin to customize responses for requests rejected with a 429 Too Many Requests response.

package com.example import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlin.time.Duration.Companion.seconds fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) fun Application.module() { install(RateLimit) { register { rateLimiter(limit = 5, refillPeriod = 60.seconds) } register(RateLimitName("public")) { rateLimiter(limit = 10, refillPeriod = 60.seconds) } register(RateLimitName("protected")) { rateLimiter(limit = 30, refillPeriod = 60.seconds) requestKey { applicationCall -> applicationCall.request.queryParameters["login"]!! } requestWeight { applicationCall, key -> when(key) { "jetbrains" -> 1 else -> 2 } } } } install(StatusPages) { status(HttpStatusCode.TooManyRequests) { call, status -> val retryAfter = call.response.headers["Retry-After"] call.respondText(text = "429: Too many requests. Wait for $retryAfter seconds.", status = status) } } routing { rateLimit { get("/") { val requestsLeft = call.response.headers["X-RateLimit-Remaining"] call.respondText("Welcome to the home page! $requestsLeft requests left.") } } rateLimit(RateLimitName("public")) { get("/public-api") { val requestsLeft = call.response.headers["X-RateLimit-Remaining"] call.respondText("Welcome to public API! $requestsLeft requests left.") } } rateLimit(RateLimitName("protected")) { get("/protected-api") { val requestsLeft = call.response.headers["X-RateLimit-Remaining"] val login = call.request.queryParameters["login"] call.respondText("Welcome to protected API, $login! $requestsLeft requests left.") } } } }
01 September 2026