Skip to content
Warrantv0.1

Rate limits

Separate buckets for reads and writes, per caller, in a one-minute window.

Reads and writes have separate budgets — 600 and 60 per minute — charged to the caller rather than to the endpoint.

RateLimit-Limit: 60
RateLimit-Remaining: 58
RateLimit-Reset: 41

The two buckets

BucketMethodsDefault
readGET, HEAD600 per minute
writeeverything else60 per minute

The window is 60 seconds, fixed rather than sliding.

Writes get the tighter budget because they are the money paths. A runaway agent polling warrant state is a nuisance; a runaway agent opening warrants is an incident.

Who the request is charged to

The bucket key is the caller, not the route:

CallerCharged to
Signed-in personTheir user id
Agent with an API keyThe key id
Anything unauthenticatedThe client address

Two API keys in one organisation therefore have independent budgets, and one agent misbehaving does not throttle another.

Unauthenticated traffic is bucketed per address specifically so that one caller hammering POST /auth/session cannot exhaust sign-in for everybody else.

One process, for now

The counter lives in the API process's memory. On a single instance it is exact; across several it is per-instance, so the effective limit is the value below multiplied by the number of running instances. Upstash Redis replaces the store without changing anything on this page. Treat the numbers as a floor you can rely on, not a ceiling you can calibrate against.

Headers on every response

HeaderMeaning
RateLimit-LimitThe budget for this bucket.
RateLimit-RemainingWhat is left in this window.
RateLimit-ResetSeconds until the window resets.
Retry-AfterOn 429 only. Seconds to wait.

Retry-After and RateLimit-Reset carry the same figure on a 429. Wait that long — do not retry immediately, and do not back off exponentially past the reset, because the window is fixed and the budget is whole again at the reset.

POST /v1/warrants HTTP/1.1
Host: api.gowarrant.xyz
Authorization: Bearer wr_test_8f0c1b4e2b1a4d599d0e3a2f7c1b9e44
Idempotency-Key: 0d8f1a1e-6f2c-4c1a-9a3f-2b6e5d4c3b2a
Content-Type: application/json

{ "amount": "8400000", "payee": "api.exampledata.io", "deadline": "2026-08-16T12:00:00.000Z", "spec": { "clauses": [{ "kind": "http.status", "equals": 200 }] } }

429

RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 41
Retry-After: 41

{
"error": {
  "type": "rate_limited",
  "code": "rate_limit_exceeded",
  "message": "Over 60 requests in this window. Retry after the reset.",
  "doc_url": "https://docs.gowarrant.xyz/api-reference/errors#rate_limit_exceeded",
  "request_id": "req_9c2a41f0-3b7d-4a51-8e2c-1f6b0d9a7c34"
}
}
curl -sS -D - https://api.gowarrant.xyz/v1/warrants \
-H "Authorization: Bearer $WARRANT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
  "amount": "8400000",
  "payee": "api.exampledata.io",
  "deadline": "2026-08-16T12:00:00.000Z",
  "spec": { "clauses": [{ "kind": "http.status", "equals": 200 }] }
}'

Retrying a write is safe

A write carries an Idempotency-Key, so retrying after a 429 replays rather than duplicates. Reuse the same key on the retry — a fresh key is a fresh warrant.

The bench has its own budget

POST /bench/run is limited to 30 per minute rather than 60. It reads and writes nothing, so the only thing worth protecting is CPU.