Rate limits¶
Limit how often a client may call a route. A limit is a dependency: you list
it in the route, and the route’s OpenAPI document shows its 429.
let sign_ins = Spindle.Rate.create ~limit:10 ~per:Mtime.Span.min ()
let sign_in =
Spindle.post
Spindle.Path.(s "sign-in")
Spindle.Returns.text
(let+ () = Spindle.Rate.limit sign_ins ~key:Spindle.client in
Ok "in")
let () = Eio_main.run @@ fun env -> Spindle.serve env [ sign_in ]
Ten sign-ins a minute per client pass; the eleventh is refused:
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 6
...
{"error":"rate_limited","message":"That was asked too often. Please try again in a moment."}
Rate.create ~limit ~per ?burst ()allowslimitcalls everyper, anMtime.Span.t, and up toburst(default:limit) at once. Make it once, at the top level, beside the routes that read it. Alimitorburstbelow one, or apershorter thanlimitnanoseconds, which leaves no time between two calls, raisesInvalid_argumentthere.~keyis what is counted:Spindle.client(the client’s address), or anystring Dep.t– a session, a token, a path parameter. An IPv6 address is counted by its /64, since a client may change its address within one at will; an IPv4 address by itself.Retry-Afteris in whole seconds, at least one.- Behind a proxy,
Spindle.clientis the address your~trusted_proxiesvouch for; without them every request has the proxy’s address. - The counts are the app’s, timed on the server’s monotonic clock: each app serving the limit counts its own, and several processes each count their own.
- A test’s app counts on a clock that never moves, so a limit refuses and
never refills. To wait one out, give
Spindle.Test.app ~mono_clockanEio_mock.Clock.Mono.make ()and move it withEio_mock.Clock.Mono.set_time. Addeio.mockto the test’slibrariesin itsdunefile.