Guides
Errors and retries
Handle denies, timeouts, retries, and partial failures without double-counting usage.
Retry behavior should protect one product attempt, not create a new one.
Use stable keys
Every billable attempt needs a stable idempotency key.
const requestId = "req_123"
const idempotencyKey = `generate-report:${requestId}`Reuse that key when the same request is retried. Create a new key only when the customer starts a new product attempt.
What to retry
| Situation | App behavior |
|---|---|
| Network timeout before response. | Retry with the same idempotency key. |
RATE_LIMITED response. | Back off and retry only if the product experience allows waiting. |
HARD_CAP or INSUFFICIENT_CREDITS. | Do not retry automatically; show an upgrade, top-up, or usage-limit message. |
METER_NOT_FOUND or INVALID_REQUEST. | Treat as integration configuration work, not a customer retry. |
| Reservation commit fails after work succeeds. | Retry commit with the same commit key. |
| Worker fails after reserve. | Release with the same release key. |
Minimal handler
const decision = await runtime.enforce({
requestId,
meterKey: "generation-runs",
quantity: 1,
subject: { type: "customer", key: "workspace_acme" },
idempotencyKey,
})
if (!decision.allowed) {
return {
status: decision.reasonCode === "RATE_LIMITED" ? 429 : 402,
body: {
reasonCode: decision.reasonCode,
retriable: decision.retriable,
requestId,
},
}
}Use Console evidence to debug unexpected denies. Search by request ID, subject, meter, reason code, or idempotency key.