Skip to content

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…" },
);

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.

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.

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.
  • Retries are safe with an Idempotency-Key header: 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 a 429, a 5xx, a timeout and a 402 plan_limit_reached the same way: Limitry is unavailable, and the fail mode decides. See How Limitry works.
  • The key needs the checks:write scope.
  • 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-Timing header 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), and total. Within limiter, limiter_saved is the wait for the count to be saved before you’re answered. Send limitry-timing: trip to also see trip: one trip there and back with nothing counted, and limiter_at, the Cloudflare location where your counts are kept (it adds that trip, so use it to measure, not in production). Compare total with what your code measures to see how much is network. edge names 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.