Credit Leases and Reservations
A company has 500 credits. Two agents start work at the same time, each needing up to 400 credits. Each reads 500 remaining, each starts, and between them they can consume 800 credits against a balance of 500.
Leases and reservations prevent this. Both reserve credits from the company’s balance before the work that consumes them runs, so the credits are debited when the work is authorized rather than when it completes. A balance read while work is in flight already accounts for that work. When the work finishes, the caller commits what was actually used, and the rest returns to the balance.
Schematic has two kinds: leases and reservations. They differ in where the gating decision is made. A lease moves a block of credits into the SDK, which then decides each check locally against that block. A reservation is one server call per operation.
A lease is a batch of credits reserved for many operations, so the SDK does not have to ask the server whether enough credits remain before each one. A backend SDK acquires a lease once, then allocates each request’s share out of it in local memory and decides the check there. The server is contacted only to acquire, extend, or release the lease and to record usage against it. Use a lease for high-volume gating, where a round trip per check would be too slow.
A reservation reserves credits on the server for one operation. Every reservation is a round trip. You take it, run the work, and settle it with the usage you recorded. Use it when an operation takes seconds rather than milliseconds, such as an external lookup or an inference call, when you are calling the API directly rather than through a backend SDK, or when the deployment cannot run DataStream.
Examples
Two agents drawing on one balance
A company has 500 credits. Two agents begin work at the same time, each requiring up to 400 credits, and both acquire a lease.
- The first agent reserves 400. The balance reports 100 remaining, 400 reserved, and 500 settled.
- The second agent reserves 400 and receives the same lease, because the company and credit type match. No second debit occurs, and
granted_amounton its response is 400, the size of the existing lease rather than the amount it asked for.
Because the lease is shared, its unspent portion is a shared budget rather than a per-caller allowance. The two agents have 400 credits to divide, not 400 each, and each needs to size its work against what the lease has left rather than against granted_amount. From here it can go two ways.
The work fits inside the lease. Both agents commit as they finish, 260 and 90, bringing the committed amount to 350 of 400. Release returns the unspent 50, and the balance reports 150 settled, which is 500 less the 350 consumed.
The work needs more than the lease covers. The first agent commits 260 as before, leaving 140 unspent in the lease. The second agent’s work turns out to need 300 rather than 90, so what the lease has left will not cover it.
An agent reads the lease’s unspent portion as granted_amount less tracked_amount on the acquire or extend response, as creditReserved on the entitlement a check returns, or as the reserved value on the company’s credit balance, and extends by the shortfall when its remaining work exceeds it. The backend SDKs do this on their own, extending in the background once the lease’s unspent balance falls below a configured low-water mark.
- The second agent extends by 160, the shortfall between the 300 it needs and the 140 available. Only 100 credits are drawable, because the original acquire already took 400 of the company’s 500. The lease grows to a
granted_amountof 500, and the balance reports 0 remaining, 240 reserved, and 240 settled. - Still needing 300 against the 240 the lease now has, the second agent extends again by 60. Nothing is drawable, so the lease is returned unchanged at 500. This is not an error, and no exception is raised.
- The second agent commits the 240 the lease covers and stops. The balance reports 0 settled, and release returns nothing.
Compare granted_amount before and after an extend to determine whether it added anything, because an extend that grew the lease by nothing is indistinguishable in shape from one that succeeded. Treat an unchanged granted_amount as a signal to stop rather than to proceed.
Without a lease in either case, both agents would read 500 remaining and each would authorize up to 400 credits of work, exposing the balance to 800 credits of consumption against the 500 it holds. In the second case they would have consumed 560 and overdrawn it.
One inference call
A company has 100 credits. A worker is about to run an inference call that costs at most 25 credits, and it takes a reservation for that amount.
- The reservation debits 25. The balance reports 75 remaining, 25 reserved, and 100 settled.
- The call runs and uses 18 credits. The worker sends a track event carrying the
reservation_idand a quantity of 18. - The reservation settles. The unspent 7 return to the grants they came from, and the balance reports 82 remaining, 0 reserved, and 82 settled.
If the balance had been 20 rather than 100, the reservation would have failed with a 402 and consumed nothing. A reservation is funded in full or not at all.
If a second worker takes its own reservation for 25 while the first is open, it gets a separate reservation. The balance reports 50 remaining and 50 reserved. Each settles independently.
A multi-step workflow
A three-step document workflow estimated at 900 credits acquires a lease for that amount up front, then commits after each step as that step’s actual cost becomes known:
Release then returns the unspent 175 to the balance.
Reserve once for the whole flow rather than once per step. You then authorize the full cost while abandoning the work is still cheap, instead of hitting a depleted balance partway through. See Incremental commits across a multi-step workflow for what to account for.
Lifecycle
A lease supports four operations.
The pattern corresponds to an authorization and capture on a payment card. Acquire authorizes an upper bound, extend raises that upper bound while the lease is open, commit captures the amount actually used, and release returns the remainder.
A reservation supports reserve, settle, and release. It cannot be extended, and it settles on the first track event that carries its id. See Reservations.
Balance values while credits are reserved
While a lease or reservation is open, a company’s credit balance is reported as three values.
Use settled for customer-facing balances. It changes only when work completes, not when credits are reserved or released, so it does not fluctuate while jobs are running. Use remaining for enforcement decisions; it decreases as soon as credits are reserved.
Leases
A lease lets the SDK gate many operations against credits it already has, instead of asking the server before each one. The sections below describe how a lease behaves on the server. See Using an SDK for how the SDK draws on it locally, and Reservations for the per-operation reserve.
One lease per company and credit type
Schematic maintains at most one active lease for a given company and credit type. A second acquire against the same company and credit type does not create a second lease; it returns the existing one, and both callers commit their usage against it.
Acquire calls for the same company and credit type are serialized, so two callers arriving simultaneously receive the same lease rather than two leases backed by the same credits. This is the mechanism behind the concurrency guarantee: concurrent workers draw against a single lease that was debited from the balance once, rather than against separate leases that may overlap.
The guarantee is scoped to a single company and credit type. Different credit types, and different companies, lease independently of one another.
Release once the work is done, not once per operation
Release closes the lease for everyone drawing against it. Acquire, commit, and release as a unit reads naturally when you write it for one operation, and it comes apart the moment a second operation for the same company and credit type overlaps: whichever operation finishes first closes the shared lease and returns the portion covering work that is still running.
The operation still in flight sees three things.
- Its commit falls through to the grants, because a commit against a released lease debits the balance directly. Usage is still billed exactly once.
- Its release returns a 409,
credit lease has already been released. - It runs unprotected from the moment the lease closed, so the balance it was gating against is drawable again by anything else.
Scope the lease to the batch of work instead. Acquire once with an expires_at covering the whole batch and a requested_amount sized for all of it, commit per operation, and release when nothing is left in flight. An open lease costs the reserved credits and nothing else, and expiry returns them within about a minute if the process dies before it releases.
Do not use a lease to reserve credits for a single operation. Once a second operation for the same company and credit type overlaps the first, the second acquire returns the existing lease rather than adding to it, so the second operation is not covered, and the first release closes the lease while the second operation is still running. Use a reservation instead. Each reservation is independent, and several can be open for the same company and credit type at once.
Committing more than a lease covers
Committing more than a lease covers is not blocked. The committed amount is allowed to exceed granted_amount, because capping it in the API would silently discard real usage. The caller is the enforcement point.
Suppose the second agent above commits all 300 rather than stopping at the 240 the lease covers, bringing the committed total to 560 against a granted_amount of 500.
- The commit is recorded in full. Usage is never dropped.
- The grant was debited 500, the reserved amount, at acquire and extend. The excess 60 is not deducted from any grant, because a commit against a lease draws down the lease rather than the balance.
- Reserved reports 0 rather than a negative value.
- Release returns nothing, since there is no unspent portion to return.
The consequence is that the reported balance overstates the company’s position by the overdrawn amount. Settled reads 0 while actual consumption was 560 against a balance of 500. Gate on the lease’s unspent portion so this does not arise, and extend rather than over-commit when a step’s real cost exceeds the estimate.
Incremental commits across a multi-step workflow
A lease does not have to be committed in one operation. A long-running workflow can reserve enough credits to cover the whole flow up front, then commit after each step as that step’s actual cost becomes known. Each commit reduces the lease’s unspent portion, so the reserved value tracks progress through the workflow. The multi-step workflow example above shows the numbers.
Four things to account for when a lease spans a workflow rather than a single call.
Set expires_at to cover the whole workflow. The default is five minutes, which is shorter than most multi-step flows. Set it explicitly, or extend the lease as the work proceeds if the duration is not predictable in advance.
A commit against a released lease debits the grant directly. If the lease has already been released, whether explicitly or by the expiry sweep, a track event carrying its lease_id is not applied to the lease. It falls through and debits the company’s grants instead, so the usage is billed once rather than dropped or debited twice. The remaining steps are no longer covered by a lease, and the lease’s unspent portion has already been returned to the balance. The same fall-through applies to a lease_id belonging to a different company or credit type.
Use a distinct idempotency key per step. Each step is a separate commit, so a retry without its own key is recorded as an additional draw against the lease.
A step can commit more than the lease covers. Extend the lease when a step’s real cost exceeds what the lease has left, rather than relying on the lease to stop it. See Committing more than a lease covers for what happens if a step commits past it.
Expiry
Every lease has an expiry. If expires_at is not supplied, it defaults to five minutes. Set it explicitly for longer-running work, or extend the lease while the work is in progress.
A lease that reaches its expiry without being released is released automatically, and its unspent portion returns to the balance. A sweep runs once per minute, so credits reserved by a process that terminated between acquire and release are returned within approximately a minute rather than at the end of the billing period. Expiry does not forfeit credits.
Released credits return to the specific grants they were drawn from and retain each grant’s original expiry date. A lease drawn partly from a grant expiring this month and partly from one expiring next year returns to both, so consumption order and grant expiration behave as they would have without the lease.
Partial funding
An acquire for more than the balance can cover does not fail. It returns a lease for the amount the balance could back, reported as granted_amount on the response. An acquire fails only when no credits are drawable at all.
Read granted_amount rather than assuming the requested amount was granted. A caller that requested 1,000 and received 340 must size its work to 340.
Reservations
A reservation reserves credits for one operation. Taking it debits the full amount from the company’s grants, and it closes when you report what the operation actually used.
Take one with POST /billing/credits/reservations, passing the company, the credit type, and the amount.
The debit happens under a row lock, so when two operations race for the last of a balance, exactly one succeeds. A balance that cannot cover the full amount returns a 402 and consumes nothing. Reservations are never partially funded, unlike leases; see Partial funding. Send an idempotency_key to make retries safe. The same key returns the original reservation, and the same key with a different company, credit type, or amount returns a 409.
Many reservations can be open at once for one company and credit type, alongside a lease if one exists. The company’s reserved balance is the sum of every open lease and reservation.
Settling a reservation
Settle a reservation by sending a normal track event with reservation_id set to the reservation’s id. The first track carrying the id closes the reservation.
- The tracked usage is recorded against the reservation.
- The unspent remainder returns to the grants it was drawn from.
- Usage beyond the reserved amount is billed as overage, which can push the balance negative.
Send exactly one settling track. A second track carrying the same reservation_id falls through to a normal grant debit, because the reservation is already closed, so use the events API’s idempotency key when you retry. A track body accepts lease_id and reservation_id, and lease_id takes precedence when both are present. Send one or the other.
The events pipeline settles asynchronously. Do not expect the balance or the reservation’s settled_amount to reflect a track the moment you send it.
Releasing and expiry
PUT /billing/credits/reservations/{id}/release cancels a reservation and returns the whole reserved amount. Use it only when the operation did not happen. It is idempotent, so releasing a reservation that is already released or settled returns the row unchanged.
expires_at defaults to one minute from now and may be at most one hour out. A sweep runs every ten seconds and refunds any reservation that reaches its expiry unsettled. Size expires_at above the operation’s expected duration plus a few seconds of margin. A track that arrives after expiry is still billed, as a normal grant debit.
Checking and reserving in one call
POST /flags/{key}/check-and-reserve evaluates a flag exactly as a plain check does and, when the flag allows and the matched entitlement is credit metered, takes the reservation in the same call. One round trip decides whether the operation may run and reserves what it will cost.
The response is the normal check response with a reservation object added, carrying the reservation’s id, its credits_reserved, and the event_subtype your settling track should send.
When the balance cannot fund the reservation, the response is a 200 with value false and the reason Insufficient credits, rather than the 402 the reservations endpoint returns. A flag that denies, a feature that is not credit metered, and a credit whose ledger lives on an external billing provider all return the plain check result with no reservation, and you track usage as usual. The check is logged like any other.
The endpoint requires a secret API key. Publishable keys cannot draw down a balance.
Using an SDK
The backend SDKs support both leases and reservations through their check and track-with-reservation methods, in two modes. Frontend SDKs do not take leases or reservations, because a publishable key cannot draw down a balance.
Client mode maintains one lease per company and credit type and issues per-request reservations against that lease locally, so an individual check does not require an API round trip, and reservations are gated across processes through a shared cache. It requires DataStream. Use it for gating high volumes of inference calls or agent runs.
Server mode makes one check-and-reserve call per check and settles it with a track event. It requires neither DataStream nor Redis.
Each SDK exposes a credit lease mode setting. The default is auto, which picks client mode when DataStream is enabled and server mode otherwise. Each SDK reference covers configuration, failure behavior, and pre-warming.
Without an SDK, call the endpoints in Using the API directly. For a single operation, use check and reserve, or take a reservation and settle it with a track event.
Technical details
In client mode the SDK keeps one lease per company and credit type and issues per-request reservations out of it. The tunable settings are the lease size, the lease and reservation lifetimes, and the top-up threshold. The names below follow the SDK’s naming conventions, so they vary in case between languages.
An overrides map sets any of these per credit type, keyed by credit type ID.
Two behaviors are worth understanding before changing the defaults.
The reservation TTL should exceed the longest gap between a check and its settle. A reservation that expires first returns its credits to the lease. The settle still bills the usage, and the track event’s idempotency key means a late or retried settle cannot double-bill, but the local lease balance reads high until the lease rolls over.
A check that cannot be gated fails closed. When the API is unreachable, the shared cache is down, or the lease is exhausted, the check denies. Failing open is set per check, for callers where a denial is worse than letting the work through, and it bypasses only the credit gate. Plan targeting, overrides, and every other rule on the flag still run.
Lease and reservation state lives in the same shared cache as the SDK’s flag and company data. Across several processes that cache has to be shared, or each process gates against a lease balance only it can see.
Using the API
Lease endpoints
Acquire a lease with Acquire credit lease:
The response contains the lease’s id, its granted_amount, and the tracked_amount committed against it so far. Size each operation against the difference between the two, which is what the lease has left. Acquire, extend, and release all return the same shape, so an extend tells you the new headroom without a second call.
Commit usage against the lease by sending a track event with lease_id set:
Include an idempotency key on the track event. Without one, a retried commit is recorded as an additional draw against the lease.
Release the lease with Release credit lease, once every operation drawing against it has committed. A release while another operation is still running closes the lease under it; see Release once the work is done, not once per operation.
To increase an open lease, Extend credit lease accepts an additional_amount and an optional new expires_at.
Reservation endpoints
Reserve credits for a single operation with Reserve credits:
amount must be positive. The full amount is reserved, or the request fails. expires_at is optional, defaults to one minute out, and may be at most one hour out. The response carries the reservation’s id, its reserved_amount, the settled_amount recorded so far, and released_at, which is null while the reservation is open.
Settle the reservation with a track event carrying reservation_id:
Cancel a reservation whose operation never ran with Release credit reservation, which takes no body:
To check a flag and reserve credits in one call, Check and reserve flag accepts the body a check accepts plus quantity, expires_at, and preflight. quantity defaults to 1, and the reserved amount is that many times the entitlement’s consumption rate, unless preflight.credit_cost names the entitlement’s credit, in which case that cost is reserved instead.
Ledger entries
Each operation is recorded in the credit ledger. Ledger entries carry a usage reason, and leases and reservations introduce four of them.
A lease that reserved 400 and consumed 350 produces three sets of entries: the 400 debit, the committed usage, and the 50 return, each with its own timestamp. No netting is applied. Reservation rows are kept after they settle or release, so the ledger still reconciles against them.
Related
- Credit Burndown Billing Model - credit types, grants, balances, and expiry.
- Meter on a real-time credit ledger - why enforcement needs a transactional ledger rather than a warehouse snapshot.
- Spend & Usage Controls - the limits and top-up settings a balance sits inside.
- Creating a metered feature - sending the usage events that commit against a lease or reservation.