Skip to content
Runtime

How to retry sandbox API calls safely with idempotency keys

Send the same Idempotency-Key with the same body. For 24 hours, Runtime returns the first result again instead of acting twice.

On Runtime, every write already carries a key. Both SDKs retry with that key. So a retried create never makes a second sandbox, and a retried command never runs twice. This matters for cost as well as correctness. A duplicate 2 vCPU, 4 GiB sandbox left waiting costs $0.03125 an hour, or about $22.50 over 30 days, at the rates checked on 25 October 2026 (pricing). Choose a key yourself only when a retry must survive a process restart.

What the SDKs already do

A write is any change, such as a create, an exec, a pause or a file write. The SDK makes a key for each call and keeps that key through its own retries.

Failure What the SDK does
Timeout or dropped connection Retries with the same key and a growing delay
429 rate_limited Waits retryAfterMs, then retries with the same key
502, 503, 504 Retries with the same key
no_credit_running_limit, no_capacity A create waits for room, up to two minutes by default
spending_limit_reached Not retried: a person has to raise the limit
Any other 4xx Thrown immediately as a typed error with a code and a hint

The typed error keeps the key it sent. It is error.idempotencyKey in JavaScript and error.idempotency_key in Python. Log it with the requestId (errors and retries).

Choose your own key when the process can die

A worker that crashes after it sends a create, but before it saves the sandbox ID, cannot tell whether the create happened. Derive the key from the job, not from the attempt. Then the worker that picks up the job again gets the same sandbox.

Each example below chooses a new job ID once, then reuses it for both calls. A real restarted worker loads the saved job ID from its queue instead. Keep that ID and the input unchanged for every retry. A new job ID means new work, so never generate one inside a retry loop.

TypeScriptimport { randomUUID } from "node:crypto";import { Runtime } from "withruntime";const runtime = new Runtime();const job = randomUUID();const input = { labels: { job } };const options = { idempotencyKey: `job-${job}-create` };const first = await runtime.sandboxes.create(input, options);// A restarted worker sends the same call again:const again = await runtime.sandboxes.create(input, options);console.log(first.id === again.id); // true: one sandbox, answered twiceawait again.stop();
Pythonfrom uuid import uuid4from withruntime import Runtimeruntime = Runtime()job = str(uuid4())first = runtime.sandboxes.create(idempotency_key=f"job-{job}-create", labels={"job": job})again = runtime.sandboxes.create(idempotency_key=f"job-{job}-create", labels={"job": job})print(first.id == again.id)  # Trueagain.stop()

Over HTTP, send the header yourself:

Terminalcurl -sS https://api.withruntime.com/v1/sandboxes \  -H "Authorization: Bearer ${RUNTIME_API_KEY}" \  -H "Idempotency-Key: job-1842-create" \  -H "Content-Type: application/json" \  -d '{"labels": {"job": "1842"}}'

What the server answers

You send Runtime returns
Same key, same body The first result again, with "replayed": true and the header Idempotency-Replayed: true
Same key, different body 422 idempotency_key_reused
A new key New work
Any key after 24 hours New work: the server has forgotten it

A replay returns the earlier answer. It is not a second action. Nothing is created, run or charged again (retries and idempotency).

Mistakes that cause duplicates

  • A fresh key on every attempt. A retry loop that makes a new UUID each time turns one lost reply into two sandboxes. Make the key once, before the first attempt, and reuse it.
  • Changing the body on a retry. Adding a label or raising timeoutSeconds, and then sending the old key, is refused with 422. That is a new request, so give it a new key.
  • Reading silence as failure. A missing response does not prove that the call failed. Retry the identical input with the same key. Before you repeat a side effect whose outcome is unknown, check files and processes (the request timed out).
  • Retrying a failed fork with its old key. A failed fork is final. The same key returns the same error. Fork again with a new key, and stop any copies that the error names in details.startedSandboxIds (forking).
  • Omitting the key in a direct MCP call. A key the server makes for a field you leave out cannot be recovered from a lost reply. Choose idempotencyKey before the first call to a tool that accepts it (MCP).

Start

Terminalprintf '%s\n' 'print(sum([125, 250, 375]))' > invoice.pynpx withruntime sandboxes run -- python3 - < invoice.py

A new account gets 100 free hours of a 1 GB machine in its first 30 days, with no card. The first run prints a link to approve in your browser.

Facts on this page were checked on 26 October 2026.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card