Checks
A check is the question your code asks before doing something that
costs: may this subject do this action, at this cost? Limitry looks at
every enabled limit that matches, and answers. Here,
may user usr_123 make an ai-call that costs 5 cents, against a
budget of 200 cents a month?
import { createClient } from "@limitry/sdk";
const limitry = createClient();
const answer = await limitry.checks.create( { subject: { kind: "user", id: "usr_123" }, action: "ai-call", cost: 5, }, { idempotencyKey: "7f9c…" },);from limitry import Limitry
limitry = Limitry()
answer = limitry.checks.create( subject={"kind": "user", "id": "usr_123"}, action="ai-call", cost=5, request_options={"idempotency_key": "7f9c…"},)limitry check user:usr_123 ai-call --cost 5curl -X POST https://api.limitry.com/v1/checks \ -H "Authorization: Bearer $LIMITRY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7f9c…" \ -d '{"subject":{"kind":"user","id":"usr_123"},"action":"ai-call","cost":5}'To check and do the work in one step, the SDKs’ guard runs your code
only when the answer is yes (see the TypeScript and
Python SDKs). The answer, for a user with 3 cents
left this month:
{ "id": "chk_9d2e…", "allowed": false, "reason": { "code": "limit_reached", "limitId": "lmt_3f0c…", "message": "Refused by AI budget: 3 cents left, resets 2026-11-01T00:00:00.000Z. Try again in 1123200 s." }, "limits": [ { "limitId": "lmt_3f0c…", "name": "AI budget", "unit": "cents", "window": "month", "rolling": false, "failMode": "closed", "remaining": 3, "resetsAt": "2026-11-01T00:00:00.000Z" } ], "retryAfterSeconds": 1123200}cost is how many of the limit’s units this action uses: cents for a
budget in cents, tokens for one in tokens, 1 for “one job”. It is not
money unless the unit is. It defaults to 1. A refused check uses
nothing, so the user’s 3 cents are still there for a cheaper call.
Reading the answer
Section titled “Reading the answer”Every decision is HTTP 200, allowed or not, so branch on allowed. An
error status means the request itself failed: a bad key, an invalid
body, or calling too fast.
limits lists every limit that applied, each with what is left on it
(limits[0].remaining), so you can show “3 cents left this month”
without another call. When the check is refused, reason says why:
reason.code |
Why |
|---|---|
limit_reached |
A limit is used up. reason.limitId names it. |
too_many_reservations |
The subject already holds the most open reservations (1,000) at once. |
retryAfterSeconds says when to try again. It’s null when waiting
won’t help, such as an empty balance.
reason.message says the same in one sentence, for your logs or your
users. The CLI and the SDKs show it as it is. Show it, but don’t parse
it: its wording may change. Your code branches on allowed and
reason.code.
Several limits and shared pools
Section titled “Several limits and shared pools”When several limits match, the check is allowed only if the cost fits all of them, and then it counts against all of them.
To draw from a shared pool as well, name up to 4 more subjects:
{ "subject": { "kind": "user", "id": "usr_123" }, "shared": [{ "kind": "team", "id": "team_42" }], "action": "ai-call", "cost": 5}Every limit on every subject must fit. Limitry charges all of them, or none. See Pools, balances and budgets.
mode |
What it does |
|---|---|
consume |
The default. Decides and counts. |
preview |
Decides without counting: “would this be allowed?” |
reserve |
Decides and reserves the cost until you commit or release it. See Reservations. |
Good to know
Section titled “Good to know”- Retries are safe with an
Idempotency-Keyheader: for an hour the same key gets the same answer and counts nothing again. The SDKs and the CLI send one for you. See Idempotency. - If Limitry can’t be reached, follow each limit’s
failMode. The SDKs do this for you, and treat a429, a5xx, a timeout and a402 plan_limit_reachedthe same way: Limitry is unavailable, and the fail mode decides. See How Limitry works. - The key needs the
checks:writescope. - Limit changes reach checks at once.
- A check uses the limits and counts of its key’s environment. A test
key (
lim_test_…) never touches live counts. See Test and live. - Allowed checks with a live key count toward your plan. Refused ones don’t. Test checks have their own allowance and aren’t billed.
- Every answer has a
Server-Timingheader saying where the time went inside Limitry, in milliseconds:auth(your key),limiter(the trip to where your counts are kept, and the count itself), andtotal. Withinlimiter,limiter_savedis the wait for the count to be saved before you’re answered. Sendlimitry-timing: tripto also seetrip: one trip there and back with nothing counted, andlimiter_at, the Cloudflare location where your counts are kept (it adds that trip, so use it to measure, not in production). Comparetotalwith what your code measures to see how much is network.edgenames the Cloudflare location and the copy of Limitry that answered, for support.
The same operation is limitry check on the CLI, and
a tool on the MCP server.