Errors & retries
Every runstate SDK error class and when it is raised, what the SDKs retry for you, and the error envelope and common codes of the HTTP API.
SDK errors
Section titled “SDK errors”Every SDK error is a RunstateError with four fields:
| Field (TS / Python) | Meaning |
|---|---|
code / code |
A stable error code. Branch on the class or the code, never on the message. |
status / status |
The HTTP status, or 0 for errors raised in your process. |
requestId / request_id |
Include it when you report a problem. |
retryAfter / retry_after |
Seconds to wait before trying again, or null / None. |
The classes are the same in TypeScript and Python:
| Class | When | What to do |
|---|---|---|
ConfigError |
Missing key or space, bad durations or amounts, arguments that can’t be hashed. | Fix the configuration or pass key. |
AuthError |
Bad key, missing permission, suspended space, or a plan limit reached. | Check the key and its permissions, or your plan. |
StoppedError |
The run was stopped, finished or passed its deadline. | Stop taking work for this run. |
TimeoutError |
A wait (limit, slots, take, together, a job result) ran out. |
Try again later, or use a longer timeout. The work on the server is unaffected. |
OnceFailedError |
A once function failed attempts times. Has attempts and message. |
Fix the cause. A fresh run tries again. |
TaskFailedError |
A queue job failed after its attempts, or a gather can’t reach need. Has jobId / job_id and reason. |
Read reason. |
BudgetExceededError |
A reservation doesn’t fit in the budget. | Don’t start the work. The function was not called. |
ResultTooLargeError |
An input or result is over 64 KB. | Store the data elsewhere and pass a reference. |
ConflictError |
Sizes in code don’t match the existing limit, slots or budget, or a queue’s attempts differ between processes. |
Make the code match, or change the resource in the console. |
LeaseLostError |
The process lost its hold on once or slots work because someone else took it over. Queue jobs don’t raise it: the worker’s result is dropped quietly. |
Stop. Don’t record a result for this work. |
UnavailableError |
runstate couldn’t be reached after retries. | Retry later. |
In Python, runstate.TimeoutError is also a subclass of the built-in TimeoutError.
What the SDKs retry for you
Section titled “What the SDKs retry for you”| Situation | Retried automatically? |
|---|---|
| Network error or request timeout | Yes: up to retries times (default 3), reusing the same Idempotency-Key. Then UnavailableError. |
| Slots all in use | Yes. The call waits for a free slot until its timeout (default 60 s), then TimeoutError. |
| Limit window used up | Yes. The call waits in a fair queue on the server until its timeout (default 60 s), then TimeoutError. |
| Rate limited or plan concurrency limit reached | Yes, honouring Retry-After. Only a timeout running out surfaces as an error. |
once function throws |
Yes. The next waiting caller runs it again, up to attempts (default 3). Then OnceFailedError. |
| Queue handler throws | Yes. The job is retried with backoff up to attempts (default 5). Then TaskFailedError. |
| Lost lease | Never. The SDK aborts context().signal. For once and slots it raises LeaseLostError. For a queue job it drops the worker’s result quietly, and the caller gets the result of the worker that finished the job. |
| Missing permission, bad key, budget too small | No. Raised straight away. |
Handling errors
Section titled “Handling errors”import runstate, { BudgetExceededError, OnceFailedError, RunstateError, StoppedError, TimeoutError } from 'runstate-sdk';
const ask = runstate.budget('model-usd', callModel, { reserve: 0.4, cost: (r) => r.usd });
async function summarize(text: string) { try { return await ask(text); } catch (err) { if (err instanceof BudgetExceededError) return null; // budget used up: the call never started if (err instanceof StoppedError) return null; // the run is over if (err instanceof TimeoutError || err instanceof OnceFailedError) throw err; if (err instanceof RunstateError) { console.error(`runstate ${err.code} (${err.status}), request ${err.requestId}`); } throw err; }}import runstate
@runstate.budget("model-usd", reserve=0.40, cost=lambda r: r.usd)def call_model(text: str): ...
def summarize(text: str): try: return call_model(text) except runstate.BudgetExceededError: return None # budget used up: the call never started except runstate.StoppedError: return None # the run is over except (runstate.TimeoutError, runstate.OnceFailedError): raise except runstate.RunstateError as err: print(f"runstate {err.code} ({err.status}), request {err.request_id}") raiseHTTP API errors
Section titled “HTTP API errors”If you call the HTTP API directly, every error uses one envelope:
{ "error": { "code": "INSUFFICIENT_BUDGET", "message": "reservation exceeds available budget", "requestId": "…" }}Codes and their HTTP statuses are a versioned contract. Branch on code, never on message. Common codes:
| Code | HTTP | Typical cause |
|---|---|---|
VALIDATION_FAILED |
400 | Malformed body or parameters, missing Idempotency-Key, payload over the size limit. |
UNAUTHENTICATED |
401 | Missing or invalid API key. |
FORBIDDEN |
403 | The key lacks the permission for this call, or isn’t allowed for this space. |
ENTITLEMENT_EXCEEDED |
403 | A plan ceiling was hit, such as active runs or active jobs. |
SPACE_SUSPENDED |
403 | The space was suspended by an owner. New work is refused; work already held can still be handed back. |
NOT_FOUND |
404 | Unknown id or name, or an id from another organization. |
CONFLICT |
409 | State conflict, or a resource declared with different sizes. |
IDEMPOTENCY_CONFLICT |
409 | An Idempotency-Key or job key reused with a different request. |
DEADLINE_EXCEEDED |
409 | The run’s deadline has passed. |
CAPACITY_UNAVAILABLE |
409 | No free slots. |
BACKLOG_FULL |
409 | The space’s queued-work limit is reached. See Limits. |
INSUFFICIENT_BUDGET |
409 | The budget can’t cover the reservation. |
RATE_LIMITED |
429 | The space’s requestsPerMinute limit was exceeded. |
SERVICE_RATE_LIMITED |
429 | Your organization used its plan’s requests for this minute. |
CONCURRENCY_LIMITED |
429 | Your organization is at its plan’s “agents working at once” limit. |
OPERATIONS_EXHAUSTED |
429 | The free plan’s monthly operations are used up. |
UNAVAILABLE |
503 | The service or the network failed. |
Every code and the endpoints that return it are in the HTTP API reference.
Send an Idempotency-Key on every mutation. Reusing a key with the same body within 24 hours replays the stored response; with a different body it returns IDEMPOTENCY_CONFLICT. The SDKs do this for you.