Rolling windows and concurrency
Rolling windows
Section titled “Rolling windows”By default, a minute, hour or day limit resets at the UTC
boundary. Say each of your API’s customers may make 1,000 requests an
hour with their key. A key could send 1,000 requests at 12:59 and 1,000
more at 13:01, and both bursts would be allowed. Set rolling: true
and the limit counts the last 60 seconds, 60 minutes or 24 hours at the
moment of each check instead.
import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.create({ name: "API per hour", subject: { kind: "apikey" }, action: "api-request", amount: 1000, window: "hour", rolling: true,});from limitry import Limitry
limitry = Limitry()
limitry.limits.create( name="API per hour", subject={"kind": "apikey"}, action="api-request", amount=1000, window="hour", rolling=True,)limitry limits create --name "API per hour" --subject '{"kind":"apikey"}' \ --action api-request --amount 1000 --window hour --rollingcurl -X POST https://api.limitry.com/v1/limits \ -H "Authorization: Bearer $LIMITRY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"API per hour","subject":{"kind":"apikey"},"action":"api-request", "amount":1000,"window":"hour","rolling":true}'Limitry counts use in 60 slices of the window: a second for a minute, a
minute for an hour, 24 minutes for a day. The oldest slice counts in
full, so a rolling limit never allows more than its amount, and may
refuse up to one slice early. In a check’s answer, resetsAt (and
retryAfterSeconds when refused) is when enough use has left the
window: the Retry-After to send with your 429. The
Rate-limit your API’s customers guide
adds a second rolling limit, 60 in any minute, against bursts.
week, month, year, total and concurrent limits can’t be
rolling. In the app,
choose In any 60 seconds, In any 60 minutes or In any 24
hours as the window.
Concurrency
Section titled “Concurrency”A limit with the window concurrent caps work in progress: amount is
how many at once. Say exports are heavy, and each workspace of your
product may run at most 3 at the same time. Each reservation takes one slot,
whatever its cost, and gives it back when it’s committed, released or
expires. The cost still counts against the subject’s other limits.
At most 3 exports at once per workspace:
import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.create({ name: "Exports at once", subject: { kind: "workspace" }, action: "export", amount: 3, window: "concurrent",});from limitry import Limitry
limitry = Limitry()
limitry.limits.create( name="Exports at once", subject={"kind": "workspace"}, action="export", amount=3, window="concurrent",)limitry limits create --name "Exports at once" --subject '{"kind":"workspace"}' \ --action export --amount 3 --window concurrentcurl -X POST https://api.limitry.com/v1/limits \ -H "Authorization: Bearer $LIMITRY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Exports at once","subject":{"kind":"workspace"},"action":"export", "amount":3,"window":"concurrent"}'Then reserve a slot while each export runs. Limitry refuses a 4th export until
one of the 3 ends. reserve gives it back when the
work finishes, or fails:
const file = await limitry.reserve( { subject: { kind: "workspace", id: "ws_42" }, action: "export" }, () => runExport(),);with limitry.reserve(subject={"kind": "workspace", "id": "ws_42"}, action="export"): run_export()- A
consumeorpreviewcheck against a concurrent limit is allowed while a slot is free, but takes none. Instant work takes no slot. - When every slot is taken,
retryAfterSecondsis when the earliest reservation expires. A release can free one sooner. - If you reserve, commit and release yourself (see
Reservations), release in a
finallyblock. A reservation you never commit or release keeps its slot until it expires.
Long-running work
Section titled “Long-running work”A reservation lasts ttlSeconds: 300 by default, 3,600 at most. For
work that runs longer, extend it as a heartbeat:
await limitry.reserve( { subject: { kind: "workspace", id: "ws_42" }, action: "export" }, async (reservation) => { for (const part of parts) { await exportPart(part); await reservation.extend(600); } },);with limitry.reserve(subject={"kind": "workspace", "id": "ws_42"}, action="export") as reservation: for part in parts: export_part(part) reservation.extend(600)limitry reservations extend rsv_… --ttl-seconds 600curl -X POST https://api.limitry.com/v1/reservations/rsv_…/extend \ -H "Authorization: Bearer $LIMITRY_API_KEY" \ -H "Content-Type: application/json" -d '{"ttlSeconds":600}'If your worker dies, it stops extending, and the slot frees itself.
A limit can’t change to or from concurrent, because one counts use
and the other counts open reservations. Its window is fixed once it
exists: create a new limit instead (see
What can change).
In reports
Section titled “In reports”Rolling limits report like any other. On the Usage page, Right now
shows each subject’s use in the last window. For a concurrent limit, the chart
shows how much work the limit let start each day, and Right now shows
the slots each subject has reserved, among subjects checked in the last hour.
For both, Limitry sends limit.reached at most once an hour per subject.