# Backups and restore of a service volume

A service that stores data has a **volume**. A backup is a snapshot of that volume that you take on demand, keep, and can roll the volume back to. These are *your* snapshots. Separately, Zaroz keeps its own disaster-recovery backups of the platform; they are not listed here and are not something you restore from.

The samples use the helper from [Using the Zaroz API](/guides/api/overview). `{resource_id}` is the id of the service's volume resource: list the service's resources with `GET /service-instances/{id}/resources` and pick the one of kind volume. Only services whose product has storage and whose cluster supports snapshots can take backups; check the `backups` entry in the service's [capabilities](/concepts/capabilities). A service without storage answers `404`, the same as one that is not yours.

## Take a backup

**curl**

```bash
B=/service-instances/$SERVICE/resources/$VOLUME/backups

curl -sS -X POST "$ZAROZ_API$B" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "before-upgrade"}'                         # 202: it runs in the background
curl -sS "$ZAROZ_API$B" -H "$AUTH"
```

```json
{
  "backups": [
    { "id": "…", "name": "before-upgrade", "status": "ready", "size_bytes": 482344960,
      "created_at": "2026-10-15T09:00:00Z", "completed_at": "2026-10-15T09:01:12Z",
      "restore_phase": null, "restore_started_at": null }
  ],
  "max_backups": 5
}
```

**TypeScript**

```ts
import { call } from "./zaroz";

const B = `/service-instances/${SERVICE}/resources/${VOLUME}/backups`;
await call("POST", B, { name: "before-upgrade" }); // omit the name for a timestamped default

for (;;) {
  const { body } = await call("GET", B);
  const b = body.backups.find((x: any) => x.name === "before-upgrade");
  console.log(b.status, `${body.backups.length}/${body.max_backups}`);
  if (b.status === "ready" || b.status === "failed") break;
  await new Promise((r) => setTimeout(r, 5000));
}
```

**Python**

```python
import time
from zaroz import call

B = f"/service-instances/{SERVICE}/resources/{VOLUME}/backups"
call("POST", B, {"name": "before-upgrade"})  # omit the name for a timestamped default

while True:
    _, body = call("GET", B)
    b = next(x for x in body["backups"] if x["name"] == "before-upgrade")
    print(b["status"], f"{len(body['backups'])}/{body['max_backups']}")
    if b["status"] in ("ready", "failed"):
        break
    time.sleep(5)
```

- A backup starts as `creating` and becomes restorable at `ready`. `failed` stays in the list until you delete it.
- `max_backups` is the ceiling per volume, published in the listing so you never hard-code it. Past it, creating answers a quota error: delete an old one first.
- You cannot start a backup while another backup or a restore is running on the same volume (`409`).
- A backup is a snapshot of the volume as it is at that moment. When you need a copy that is consistent at the application level (a database mid-transaction), stop writing first or take the database's own dump as well.

## Restore

**Restoring is irreversible.** The workloads that use the volume are stopped, the volume's data is reverted to the snapshot, and then they start again: **every change made since the backup was taken is lost.** If you might want the current state back, take a backup of it first. The backup must be `ready`.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API$B/$BACKUP/restore" -H "$AUTH"      # 202; poll the listing
```

**TypeScript**

```ts
await call("POST", `${B}/${BACKUP_ID}/restore`);
for (;;) {
  const { body } = await call("GET", B);
  const b = body.backups.find((x: any) => x.id === BACKUP_ID);
  console.log(b.status, b.restore_phase);
  if (b.status === "ready" && !b.restore_phase) break;
  await new Promise((r) => setTimeout(r, 10_000));
}
```

**Python**

```python
call("POST", f"{B}/{BACKUP_ID}/restore")
while True:
    _, body = call("GET", B)
    b = next(x for x in body["backups"] if x["id"] == BACKUP_ID)
    print(b["status"], b["restore_phase"])
    if b["status"] == "ready" and not b["restore_phase"]:
        break
    time.sleep(10)
```

While it restores the backup shows `status: "restoring"` and a `restore_phase` that moves through `stopping_workloads`, `detaching_pre`, `maint_attaching`, `reverting`, `detaching_post`, `csi_retaining_pv`, `csi_recreating_pvc` and `restarting_workloads`. It runs over several minutes; large volumes take longer. Do not start another backup or restore meanwhile (`409`). Use `restore_started_at` to show "restoring for N minutes".

## Delete a backup

```bash
curl -sS -X DELETE "$ZAROZ_API$B/$BACKUP" -H "$AUTH"      # 204
```

```ts
await call("DELETE", `${B}/${BACKUP_ID}`);
```

```python
call("DELETE", f"{B}/{BACKUP_ID}")
```

A backup that is `creating` or `restoring` cannot be deleted; wait until it is `ready` or `failed`. Deleting removes the underlying snapshot.

## How full is the volume

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/resources/$VOLUME/volume" -H "$AUTH"
# {"capacity_bytes": 10737418240, "used_bytes": 2147483648, "used_fraction": 0.2,
#  "used_label": "2.0 GiB of 10 GiB", "mount_path": "/data", "last_backup_at": "2026-10-15T09:01:12Z"}
```

```ts
const v = await call("GET", `/service-instances/${SERVICE}/resources/${VOLUME}/volume`);
console.log(v.body.used_label, v.body.last_backup_at);
```

```python
_, v = call("GET", f"/service-instances/{SERVICE}/resources/{VOLUME}/volume")
print(v["used_label"], v["last_backup_at"])
```

`capacity_bytes` is the configured size (a quota, always present). `used_bytes` is measured inside the cluster and is `null` whenever it cannot be measured, so show "unknown" rather than zero. `last_backup_at` tells you whether your routine is working.

## Before you rely on it

- Backups belong to the volume and its subscription. Do not treat them as the only copy of anything you cannot lose if you might delete or cancel the service.
- To keep a copy **outside** Zaroz, copy the files (see the file routes, or SFTP) or dump the database to [object storage](/storage/quick-start).
- Automate it: a nightly job that creates a backup, waits for `ready` and deletes the oldest when you reach `max_backups` is a few lines of the code above. Use a [token](/api-tokens/getting-started) with `services.manage` scoped to that one service.
