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: 41The two buckets
| Bucket | Methods | Default |
|---|---|---|
| read | GET, HEAD | 600 per minute |
| write | everything else | 60 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:
| Caller | Charged to |
|---|---|
| Signed-in person | Their user id |
| Agent with an API key | The key id |
| Anything unauthenticated | The 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
| Header | Meaning |
|---|---|
RateLimit-Limit | The budget for this bucket. |
RateLimit-Remaining | What is left in this window. |
RateLimit-Reset | Seconds until the window resets. |
Retry-After | On 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.