Ephemeral sandboxes

Machines that are guaranteed to disappear

Ephemerality is not a cost feature that happens to be convenient. It is what makes the isolation argument hold: a machine that cannot outlive its timeout cannot quietly accumulate state, credentials or a foothold. This page is about designing around that guarantee rather than fighting it.

Two clocks, and only one of them is a guarantee

Every sandbox has an expiry, set at creation with timeout_minutes and enforced by the platform. When it arrives the machine is destroyed and billing stops, whether or not anything asked for that. Separately, you can call DELETE to end it early.

Those two are not equivalent, and treating them as though they were is the most common design error. DELETE is an optimisation: it returns the minutes you were not going to use. The timeout is the guarantee: it survives your process crashing, your container being rescheduled, your agent deciding the task is finished mid-loop, and the network partition that made you unsure whether the create call even succeeded. Write your code so that forgetting to call DELETE costs money and nothing else.

Reconciliation underneath both

There is a third layer you do not interact with. A reconciler continuously compares what the control plane believes is running against what is actually running, and destroys anything unaccounted for. This matters because the failure that leaves a machine alive forever is not a customer forgetting to call delete — it is a bug in the platform's own bookkeeping. Ephemerality that depends on the same records that could be wrong is not much of a guarantee.

Lifecycle patterns

Short timeout, explicit extend

Create with roughly twice the expected runtime. When a job overruns, call POST /v1/sandboxes/{id}/extend from the code that knows it is still making progress. This inverts the usual failure: a leak ends by itself, and only live work stays alive.

Scope the sandbox to a unit of work

One sandbox per agent run, per evaluation, per user submission. It gives you independence between units for free — a run that fills its disk or leaves a daemon running cannot affect the next one — and it makes the accounting obvious, because one sandbox's minutes are one unit's cost.

Destroy in a finally block

Teardown belongs in whatever construct your language guarantees will run: finally, a context manager, defer. Not at the end of the happy path, where the exception you did not anticipate skips it.

Make idempotent creates the default

Send an Idempotency-Key on create. A timeout on the create call is exactly the moment you cannot tell whether a machine exists, and a blind retry is how you end up paying for two.

Getting state across the boundary

import os, contextlib, requests

API = "https://sandbox-as-a-service.com/v1"
H = {"Authorization": f"Bearer {os.environ['AAS_API_KEY']}"}

@contextlib.contextmanager
def sandbox(size="small", minutes=15, key=None):
    hdrs = {**H, **({"Idempotency-Key": key} if key else {})}
    sid = requests.post(f"{API}/sandboxes", headers=hdrs,
                        json={"size": size, "timeout_minutes": minutes}).json()["id"]
    try:
        yield lambda cmd, **kw: requests.post(
            f"{API}/sandboxes/{sid}/exec", headers=H,
            json={"command": cmd, **kw}).json()
    finally:
        # Optimisation, not the guarantee. The timeout is the guarantee.
        requests.delete(f"{API}/sandboxes/{sid}", headers=H)


with sandbox(minutes=20, key=f"job-{job_id}") as run:
    # IN: rebuild the environment from a script, never from a snapshot.
    run("git clone --depth 1 $REPO_URL work",
        env={"REPO_URL": short_lived_clone_url}, timeout_ms=120_000)
    run("cd work && pip install --user -qr requirements.txt", timeout_ms=300_000)

    # WORK
    r = run("cd work && python3 -m pytest -q 2>&1 | tail -n 50", timeout_ms=600_000)

    # OUT: small results through stdout, large artefacts to your own storage.
    report = r["stdout"]
    run("cd work && tar czf - artifacts | curl -s -T - $UPLOAD_URL",
        env={"UPLOAD_URL": presigned_put_url}, timeout_ms=120_000)
# A long job that reports progress: extend only while work is live.

deadline_pushes = 0
while not finished and deadline_pushes < 6:
    tail = run("tail -n 5 build.log")["stdout"]
    if made_progress(tail):
        requests.post(f"{API}/sandboxes/{sid}/extend", headers=H,
                      json={"additional_minutes": 15})
        deadline_pushes += 1
    time.sleep(60)

# If progress stops, extension stops, and the sandbox expires on its own.
# A stuck job costs at most one more timeout window — never an open bill.
# The ceiling is 24 hours of total lifetime regardless.

What ephemerality buys, beyond the bill

A machine that is destroyed on a schedule cannot drift. There is no configuration that accumulated over six months, no package installed by hand that nobody documented, no leftover process from a run in March. Every sandbox starts from the same base, which means the setup script is the environment definition — and a setup script that has to work every single time is one that stays correct.

It also bounds compromise in time. An attacker who gets code execution in a sandbox has, at most, the remainder of that sandbox's lifetime, on a machine nobody else shares, that will be destroyed rather than reset. Persistence — the step that turns an incident into a breach — has nowhere to live. That is a weaker property than preventing the compromise, and a much easier one to actually guarantee.

The cost is that you must externalise anything you want to keep. In practice that discipline is worth having on its own: results in your storage, environments defined by scripts, and nothing important living only on a machine you do not control.

Questions

What happens if my process dies before it calls DELETE?

Nothing bad, which is the point. The sandbox carries a timeout set at creation — 15 minutes by default, 24 hours maximum — and is destroyed when it expires regardless of whether anything asked. Billing stops at destruction. Your DELETE call is a cost optimisation that returns the minutes you were not going to use; it is not what makes cleanup happen.

Can I keep a sandbox alive indefinitely?

No. 24 hours is a hard ceiling, and extend moves the expiry within that ceiling rather than removing it. If you need a long-lived development environment, this is not the right service — its guarantees come from the fact that nothing here is long-lived.

How do I keep state across sandboxes?

Externalise it deliberately: commit and push to a repository, upload artefacts to your own object storage from inside the sandbox, or return small results through stdout. The useful discipline is to write the setup as a script that can rebuild the environment from nothing, then a lost sandbox costs minutes rather than work.

Is the disk really gone?

The machine and its disk are destroyed rather than reset and re-leased, and machines are never handed to a second account. A reconciler independently compares recorded state against what is actually running and destroys anything unaccounted for, so a control-plane bug that loses track of a machine does not leave it alive with your data on it.

What is the right default timeout?

Roughly twice your expected runtime, and short. A 15-minute default with an explicit extend when work overruns is better than a long timeout set to avoid ever thinking about it, because the timeout is your last line of defence against a leak. Extending is a single call and costs nothing until the minutes are used.

Does an idle sandbox cost the same as a busy one?

Yes. Billing follows wall-clock runtime from ready to destroyed, not CPU consumption, so a sandbox waiting on your orchestrator costs exactly what one running a build costs. That is the strongest practical argument for destroying eagerly instead of relying on the timeout.

Create something you can throw away

Prepaid credit, billing by the second, and a timeout that cleans up after you.