sturdy-throttle is a two-tiered rate limiting library for Clojure ring apps.
It is designed to provide both DoS protection using a fast in-memory atom, and structured API quota enforcement using a SQLite table.
429 Too Many Requests responses to save bandwidth against DoS attacks.organization_id and an optional rate_key. The quota window is configurable and defaults to one hour. It automatically migrates the required schema on startup and prunes old buckets in the background.The SQLite limiter deliberately approximates a rolling window with wall-clock-aligned, one-minute buckets.
It rounds :window-ms up to a whole number of minute buckets and sums the current bucket plus the preceding live buckets.
Consequently, enforcement boundaries can differ from an exact per-request rolling window by less than one minute.
Sub-minute windows use one calendar-minute bucket, and traffic can burst when an old bucket leaves the window at a minute boundary.
This is an intentional performance tradeoff. Aggregating requests into one row per organization, rate key, and minute keeps the table and write volume small, and allows quota checking and incrementing to remain a single atomic SQL statement under load. Use this limiter when minute-level approximation is acceptable rather than when the quota contract requires exact elapsed-time semantics.
Pruning is scoped to the rate key that triggers it, so different rate keys may safely use different windows in the same database.
The same rate_key must not simultaneously be used by two limiters with different windows in one database; their quota buckets intentionally share the same key space.
When changing a rate key to a longer window, buckets already removed under the shorter policy cannot be recovered, so the longer window has a warm-up period while new history accumulates.
Add to deps.edn:
{:deps com.sturdystats/sturdy-throttle {:mvn/version "VERSION"}}
Initialize rate limiters during your application's startup sequence and store them in your system state.
(ns my-app.server
(:require [sturdy.throttle.memory :as memory]
[sturdy.throttle.sqlite :as sqlite]))
(defn start-limiters [_config]
;; 1. The in-memory IP limiter
;; E.g., Max 50 requests per second per IP
(let [ip-limiter (memory/make-ip-limiter {:limit-per-second 50
:window-ms 1000})]
;; 2. The SQLite-backed quota limiter
;; E.g., Max 25,000 requests per hour per org
(let [quota-limiter (sqlite/make-quota-limiter {:db-name "rate-limits"
:db-dir "/var/lib/my-app/limits"
:limit 25000
:window-ms 3600000
:prune-every 1000})]
{:ip-limiter ip-limiter
:quota-limiter quota-limiter})))
You can wrap your routes using the provided middleware functions in
sturdy.throttle.
wrap-ip-rate-limit should be applied to the entire router (pre-auth), while wrap-quota-rate-limit should apply to to specific protected routes (post-auth).
(ns my-app.routes
(:require [reitit.ring :as ring]
[sturdy.throttle :as throttle]))
(defn app-router [{:keys [ip-limiter quota-limiter]}]
(ring/ring-handler
(ring/router
["/api"
;; --- POST-AUTH QUOTA LIMITER ---
;; Apply quota limiting to all authenticated /api routes
{:middleware [[throttle/wrap-quota-rate-limit
quota-limiter
(fn [req]
;; Extract your org-id from the session/token.
;; You can also include a :rate-key to target specific endpoints.
{:org-id (get-in req [:identity :org_id])
:rate-key "api-general"})
;; Optional custom error response
{:status 429
:headers {"Content-Type" "application/json"}
:body "{\"error\": \"Hourly quota exceeded.\"}"}]]}
["/v1/resource" {:get handler}]]
{:data
;; --- PRE-AUTH IP LIMITER ---
;; Apply IP rate limiting globally across the entire router
{:middleware [[throttle/wrap-ip-rate-limit
ip-limiter
(fn [req]
;; Only trust an IP header set or sanitized by your own
;; reverse proxy (for example, Nginx's X-Real-IP).
;; Never trust a client-controlled forwarding header.
(or (get-in req [:headers "x-real-ip"])
(:remote-addr req)))]]}})))
Close the SQLite limiter in your server shutdown sequence. This closes the SQLite connection cleanly.
(defn stop-limiters [{:keys [quota-limiter]}]
(when quota-limiter
(sqlite/close-limiter quota-limiter)))
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |