# Lifecycle

A sandbox moves between a few well-defined states. The important ones for your bill are `running` (billed per second) and `paused` ($0 compute, full RAM and disk state kept).

## States

```
                    (priority: "batch" only)
                         ┌─────────┐
                         │ queued  │  waiting for capacity, not billed
                         └────┬────┘
                              ▼
   POST /sandboxes ──► ┌──────────┐
                       │ creating │  not billed
                       └────┬─────┘
                            ▼
                       ┌──────────┐  pause / auto-pause  ┌─────────┐       ┌────────┐
          ┌──────────► │ running  │ ───────────────────► │ pausing │ ────► │ paused │
          │            └──────────┘                      └─────────┘       └───┬────┘
          │                                                                    │ resume /
          │            ┌──────────┐                                            │ wake on request
          └─────────── │ resuming │ ◄──────────────────────────────────────────┘
                       └──────────┘

   any state ── DELETE / ttl expired ──► deleting ──► deleted
   any state ── VM failure ──────────► error   (see sandbox.error)
```

| State | Meaning |
| --- | --- |
| `queued` | `priority: "batch"` only; waiting for capacity |
| `creating` | VM is being started from a template, snapshot or image |
| `running` | VM is running; exec, files, ports, SSH and desktop work |
| `pausing` | RAM and disk are being snapshotted at the same instant |
| `paused` | VM is stopped; RAM and disk are kept in a snapshot |
| `resuming` | VM is being restored (target p95 < 800 ms) |
| `deleting`, `deleted` | sandbox and its data are being removed / gone |
| `error` | VM failed; `sandbox.error` has `code` and `message` |

Processes, open TCP connections, terminal sessions, desktop windows and browser tabs survive a pause: resume restores memory, it does not reboot.

## Pause and resume

```ts
await sandbox.pause();   // waits until "paused" (wait: true by default)
await sandbox.resume();  // waits until "running"
```

```python
sandbox.pause()
sandbox.resume()
```

```bash
curl -sS -X POST https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/pause \
  -H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '{"wait": false}'    # 202, state "pausing"
```

```bash
taskcloud pause sbx_2fqxnt4bkq7m
taskcloud resume sbx_2fqxnt4bkq7m
```

With `wait: true` (default) the call blocks until the transition finishes (max 60 s, then `202`). Pausing a paused sandbox or resuming a running one is a no-op that returns `200`.

## Auto-pause

Every sandbox pauses itself after `auto_pause.idle_timeout_seconds` without activity. Defaults: `enabled: true`, `idle_timeout_seconds: 300` (5 minutes). Range: 60–86400 seconds.

Activity means any of:

- a running exec command or an open PTY session;
- inbound traffic on an exposed port or over SSH;
- a connected desktop viewer, or desktop input;
- CPU usage above a low threshold.

An open file watch (`GET .../files/watch`) does **not** count as activity.

```ts
await sandbox.update({ auto_pause: { enabled: true, idle_timeout_seconds: 900 } });
```

```python
sandbox.update(auto_pause={"enabled": True, "idle_timeout_seconds": 900})
```

```bash
curl -sS -X PATCH https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m \
  -H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '{"auto_pause":{"enabled":true,"idle_timeout_seconds":900}}'

taskcloud update sbx_2fqxnt4bkq7m --idle-timeout 15m
taskcloud update sbx_2fqxnt4bkq7m --no-auto-pause
```

## Wake on request

You rarely need to call `resume`. A paused sandbox wakes automatically when it receives:

- an HTTP request to one of its port URLs (`https://<short_id>-<port>.taskhost.cloud`) or its desktop URL;
- an SSH connection through `ssh.taskhost.cloud`;
- an API call that needs a running VM: exec, PTY, files, screenshot, input.

The request waits for the resume (target p95 < 800 ms) and then completes normally, without an error on the client side. `GET /sandboxes/{sandbox_id}` does not wake a sandbox.

To fail fast instead, pass `auto_resume=false` on API calls; a paused sandbox then returns `409 sandbox_paused`:

```bash
curl -sS -X POST "https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/exec?auto_resume=false" \
  -H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '{"command":"uptime"}'
```

## Always-on

`always_on: true` disables auto-pause. Instead of hourly billing, the sandbox costs a flat $19.90 per month (prorated). Available for size `default` only.

```ts
await sandbox.update({ always_on: true });
```

```bash
taskcloud update sbx_2fqxnt4bkq7m --always-on
```

## TTL

`ttl_seconds` deletes the sandbox automatically N seconds after creation (min 60; `null` keeps it until you delete it). `expires_at` shows the deadline. Change it with `PATCH`. Expiry sends `sandbox.deleted` with `reason: "ttl_expired"`.

## Batch priority

`priority: "batch"` is billed at −50%. In exchange the sandbox may wait in `queued` until capacity is available, and the create call always returns `202` (`wait` is ignored). Use it for evals, CI and other jobs that are not interactive. Work run during the EU night window (22:00–07:00 CET) gets the same −50% automatically.

```ts
const job = await client.sandboxes.create({ template: "base", priority: "batch", wait: false });
await job.waitFor("running");
```

```python
job = client.sandboxes.create(template="base", priority="batch", wait=False)
job.wait_for("running")
```

```bash
taskcloud create --template base --batch --no-wait
```

## Billing per state

Compute is billed per second while the VM runs. Prices per active hour: `small` $0.015, `default` $0.030, `large` $0.060.

| State | Compute | Storage |
| --- | --- | --- |
| `queued`, `creating` | not billed | – |
| `running` | per second at the size rate | disk above the included amount: $0.05/GB-month |
| `pausing`, `resuming` | per second, as `running` | – |
| `paused` | **$0** | pause snapshot free for 30 days, then $0.03/GB-month; extra disk as above |
| `deleting`, `deleted` | not billed | not billed |
| `error` | not billed | disk kept until you delete the sandbox |
| `always_on: true` | flat $19.90/month (`default`) instead of per second | as above |

Batch priority and the off-peak window (22:00–07:00 CET) are billed at −50%. See [Billing](../billing.md) for plans and credit.

## Checkpoints and host failures

TaskCloud saves sandboxes to replicated object storage, encrypted with a key of your account:

- **Pause:** a paused sandbox's memory and disk are uploaded a few minutes after the pause. It can then resume on any host, even if its original host fails.
- **Running sandboxes:** after about 15 minutes of activity since the last resume or checkpoint, TaskCloud takes a **disk checkpoint**. The sandbox keeps running; there is no pause. The checkpoint is crash-consistent, like the disk after a power cut: files flushed to disk are in it, data still in the guest's page cache may not be.

If the host running a sandbox fails, TaskCloud restores the sandbox on another host from its newest saved state and sends [`sandbox.restored_from_checkpoint`](../guides/webhooks.md):

| Newest saved state | After the restore |
| --- | --- |
| pause snapshot (`kind: memory`) | memory, processes and files as of that pause |
| disk checkpoint (`kind: disk`, `ram_lost: true`) | files as of the checkpoint; the sandbox booted, so processes and `/dev/shm` restarted |

At most about 15 minutes of disk work is lost (`data_loss_window_seconds` in the webhook tells you how much). Long-running agents should keep their progress in files (and call `sync` after important steps) so they can continue from a disk checkpoint. Paused sandboxes are not affected: their pause snapshot is restored as is.

A sandbox paused less than a few minutes before its host failed may not be uploaded yet. It stays `paused` until the host returns.

## Webhooks per transition

| Event | Sent when | Typical `reason` |
| --- | --- | --- |
| `sandbox.ready` | sandbox first becomes `running` after create or fork | `api` |
| `sandbox.paused` | state becomes `paused` | `api`, `auto_pause`, `spend_limit`, `host_maintenance` |
| `sandbox.resumed` | state becomes `running` after `resuming` | `api`, `wake_on_request` |
| `sandbox.error` | state becomes `error` | an error code |
| `sandbox.deleted` | state becomes `deleted` | `api`, `ttl_expired` |
| `sandbox.restored_from_checkpoint` | sandbox runs again from an older saved state after a host failure | `host_lost`, `restore_interrupted` |

Each event carries `previous_state`, `state`, `reason` and the full sandbox. Setup and signature verification: [Webhooks](../guides/webhooks.md).

## Related

- [Sandboxes](sandboxes.md)
- [Snapshots and fork](snapshots-and-fork.md)
- [Billing](../billing.md), [Limits](../limits.md)
