Network retries are unavoidable: a request times out, you resend, and now you risk double-counting usage or enqueuing the same job twice. Gatekeeper solves this with idempotency keys. When you tag a write with a key, replaying the same write is a no-op that returns the original result instead of creating a duplicate.
Where it applies#
Two write paths accept an idempotency key, each backed by a database UNIQUE constraint so the guarantee holds even under a concurrent race:
| Write | Field | Backed by |
|---|---|---|
| Record usage / check-and-record | idempotencyKey | UNIQUE(idempotency_key) on usage_events |
| Enqueue background job | idempotencyKey | UNIQUE(idempotency_key) on background_jobs |
The key is yours to choose. Anything stable across the retries of a single logical operation works - a request id, an order id, or a UUID you generate once and reuse on resend.
Same-key behavior#
When a key has already been seen, the operation does not insert a second row. Instead it returns the row recorded the first time:
- Usage: recording with a key that already exists returns the original usage event - quantity is counted once. On the check-and-record path, a duplicate is treated as already-allowed and the prior event is returned with a fresh quota snapshot. A
UNIQUEcollision from a concurrent race is non-fatal: the loser reads back the winner's event. - Jobs: enqueuing with a key that already exists is a
DO NOTHINGinsert; the existing job is returned, so the job runs once.
Recording usage idempotently#
Pass idempotencyKey on the usage write. Send the same key on every retry of that one record.
import { GatekeeperCore, UsageService } from '@orkait/sdk';
const usage = new UsageService(core);
const key = crypto.randomUUID(); // one key per logical operation
// First call records; any retry with the same key returns the same event.
const result = await usage.checkAndRecord({
tenantId,
service: 'inference',
action: 'completion',
quantity: 1,
idempotencyKey: key,
});
if (!result.allowed) {
// 402-class outcome: over quota, nothing recorded
}Choosing a key#
- Stable across retries, unique across operations. Derive it from the operation, not the wall clock.
- Generate it before the first attempt and store it with your own record, so a retry after a crash reuses the same value.
- Do not recycle keys across different payloads - that is a
409, by design.
Related#
- Responses and errors - the
409 conflictcode - Rate limits - retrying after a
429