refrakt/rate_limit

Rate limiting middleware using a sliding window counter.

Experimental. This module is lightly used and may change or move to its own package; see https://github.com/raskell-io/refrakt/issues/28.

A single OTP actor keeps the request timestamps per client. Clients whose requests have all left their window are dropped by a periodic cleanup, so memory stays bounded by the number of recently active clients.

Usage

import refrakt/rate_limit

// Start the rate limiter (once at app startup)
let assert Ok(limiter) = rate_limit.start()

fn middleware(req, next) {
  use <- rate_limit.check(
    req,
    limiter,
    max: 100,          // max requests
    window_ms: 60_000, // per minute
    key: rate_limit.by_forwarded_for(trusted_proxies: 1),
  )
  next(req)
}

Choosing the client key

Requests are counted per key, so the key must be something a client cannot change at will. Wisp does not expose the socket peer address, so you say where the client address comes from:

// In main()
let handler = fn(req: request.Request(mist.Connection)) {
  let client_ip = case mist.get_connection_info(req.body) {
    Ok(info) -> mist.ip_address_to_string(info.ip_address)
    Error(_) -> "unknown"
  }
  wisp_mist.handler(
    router.handle_request(_, ctx, client_ip),
    cfg.secret_key_base,
  )(req)
}

// In the router
use <- rate_limit.check(req, limiter, max: 100, window_ms: 60_000, key: fn(_) {
  client_ip
})

Any fn(Request) -> String works as a key, so you can also limit per user or per API token.

Types

pub opaque type RateLimitMessage

A running rate limiter instance.

pub type RateLimiter =
  process.Subject(RateLimitMessage)

Values

pub fn by_forwarded_for(
  trusted_proxies trusted_proxies: Int,
) -> fn(request.Request(wisp.Connection)) -> String

Key requests by the client address that the app’s own reverse proxies recorded in X-Forwarded-For: the trusted_proxiesth entry from the right. Entries further left are client-supplied and ignored. Requests without the header share the "unknown" key.

pub fn by_header(
  name: String,
) -> fn(request.Request(wisp.Connection)) -> String

Key requests by a single-valued header that a trusted proxy always sets, such as cf-connecting-ip. Header names are lowercase in Wisp. Requests without the header share the "unknown" key.

pub fn check(
  req: request.Request(wisp.Connection),
  limiter: process.Subject(RateLimitMessage),
  max max: Int,
  window_ms window_ms: Int,
  key key: fn(request.Request(wisp.Connection)) -> String,
  next next: fn() -> response.Response(wisp.Body),
) -> response.Response(wisp.Body)

Check if a request is within the rate limit for its key. Returns 429 Too Many Requests if exceeded.

pub fn start() -> Result(
  process.Subject(RateLimitMessage),
  actor.StartError,
)

Start the rate limiter actor. Idle clients are forgotten once a minute.

pub fn start_with_cleanup_interval(
  every interval_ms: Int,
) -> Result(process.Subject(RateLimitMessage), actor.StartError)

Start the rate limiter actor, dropping clients with no requests left in their window every interval_ms milliseconds.

pub fn tracked_keys(
  limiter: process.Subject(RateLimitMessage),
) -> Int

The number of clients the limiter currently remembers.

✨ Search Document