> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Block storage

> Create volumes, attach them to an instance, grow them, and keep point-in-time snapshots on a schedule.

A volume is a block device that lives independently of any instance. You create
it, attach it to an [instance](/compute), and it keeps your data when that
instance goes away. Volumes are regional: they are served from
`https://storage.<region>.basaltic.sh` and can only attach to an instance in the
same region.

<CardGroup cols={2}>
  <Card title="Create a volume" icon="hard-drive" href="#creating-a-volume">
    The two tiers you can provision on, and what each one gives you.
  </Card>

  <Card title="Attach and grow" icon="expand" href="#attaching-to-an-instance">
    Single-attach, what survives instance deletion, and the one state an
    extend is accepted in.
  </Card>

  <Card title="Snapshots" icon="camera" href="#snapshots">
    Taking one, restoring from one, and why a volume with snapshots will not
    delete.
  </Card>

  <Card title="Snapshot policies" icon="calendar-clock" href="#snapshot-policies">
    Schedules, the two retention bounds, and what retention will never touch.
  </Card>
</CardGroup>

## Creating a volume

Creation is asynchronous. `POST /v1/volumes` answers **`202`** with the volume
in `creating`; poll `GET /v1/volumes/{volume_id}` until `status` becomes
`available` or `error`.

<Tabs>
  <Tab title="Console">
    Go to **Storage → Volumes** and choose **Create Volume**. Give it a
    **Name**, leave **Source** on **Blank volume**, then set **Size (GB)** and
    pick a **Tier** — **SSD** or **NVMe**. The **Tags** card takes the same
    key/value pairs the API does.

    The size field tells you the IO allowance the volume will get as you type,
    because size and tier decide it together.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST https://storage.sa-saopaulo-1.basaltic.sh/v1/volumes
    {
      "name": "app-data-01",
      "volume_type": "ssd",
      "size_gb": 100,
      "tags": { "env": "production" }
    }
    ```
  </Tab>
</Tabs>

<ResponseField name="name" type="unique within your account">
  Matches `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`. It appears in the CRN, so it
  has to be URL-safe. A duplicate name is `409 VOLUME_NAME_EXISTS`.
</ResponseField>

<ResponseField name="size_gb" type="1–16384" required>
  The ceiling matches what a single block device is capped at elsewhere in the
  industry. It is a per-volume sanity bound, not your quota — aggregate usage
  is gated separately.
</ResponseField>

<ResponseField name="volume_type" type="ssd | nvme" required>
  See below. `hdd` exists as a stored tier but is rejected for new volumes:
  the spinning-disk pool is reserved for cold object storage.
</ResponseField>

<Tip>
  Send an `Idempotency-Key` header. Retrying with the same key returns the
  original outcome instead of creating a second volume; reusing the key with a
  different body is rejected with `422`.
</Tip>

## Volume types

`GET /v1/volume-types` lists what you can provision on. The tier decides the
IO ceiling your volume gets, and that ceiling **scales with size** rather than
being flat — so the performance tracks what you paid for.

<Columns cols={2}>
  <Card title="ssd" icon="gauge">
    **12 IOPS and 0.25 MiB/s per GB.** Balanced cost against throughput.

    Floor 300 IOPS / 25 MiB/s, ceiling 16,000 IOPS / 250 MiB/s.
  </Card>

  <Card title="nvme" icon="zap">
    **30 IOPS and 0.5 MiB/s per GB.** Highest IOPS, lowest latency.

    Floor 1,000 IOPS / 50 MiB/s, ceiling 32,000 IOPS / 500 MiB/s.
  </Card>
</Columns>

Both tiers burst to **3× the sustained ceiling for 60 seconds**. The burst
budget refills while the volume sits under its baseline, so short spikes are
absorbed without you tracking anything. A burst is clamped to the tier maximum
too: bursting hides spikes, it does not hand a small volume more throughput
than the largest one can sustain.

<Info>
  The floors exist so a small volume is still usable, and the ceilings exist so
  one large volume cannot consume a meaningful fraction of the tier on its own.
  A single guest issuing enough concurrent IO can otherwise reach a
  double-digit percentage of everything the tier serves.
</Info>

The type is fixed at creation, and there is no call that changes it. Moving to a
different tier means creating a volume of the type you want and copying the data
across from inside the guest.

## Attaching to an instance

Attachment is a compute operation — the device binding, the guest hot-plug and
the boot order all live on the instance side:

<Tabs>
  <Tab title="Console">
    Open the volume from **Storage → Volumes** and choose **Attach to
    instance**. Pick the **Instance**; **Device name**, **Mount path** and
    **Filesystem** are optional. Leaving **Device name** blank assigns the next
    available name, and setting a **Mount path** mounts the volume there inside
    the guest — formatting it first only if it is blank.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/instances/{instance_id}/volumes
    { "volume_id": "5f8d2c1a-..." }
    ```
  </Tab>
</Tabs>

The volume moves from `available` to `in_use`, and detaching puts it back to
`available`. See [compute](/compute) for the device slots, mount options and the
detach call.

<Warning>
  **A volume attaches to one instance at a time.** There is no multi-attach:
  the binding is unique per volume, so a second attach is refused rather than
  handing two guests the same block device.
</Warning>

### What happens when the instance is deleted

Each attachment carries a `delete_on_termination` flag, and the default differs
by how the volume got there:

| Volume                                | `delete_on_termination` | Result when the instance is deleted |
| ------------------------------------- | ----------------------- | ----------------------------------- |
| Boot volume created with the instance | `true`                  | Destroyed with the instance.        |
| Data volume you attached              | `false`                 | Survives, returns to `available`.   |

`PATCH /v1/instances/{instance_id}/volumes/{volume_id}` changes the flag on an
existing attachment. Set it deliberately on anything holding data you care
about — the boot volume default is the one that deletes.

In the console the flag is a **Delete on termination** switch in the
**instance's** list of attached volumes, not on the volume's own page. It
belongs to the attachment, so that is where it lives.

The boot disk itself cannot be detached: unplugging it would take the guest's
root filesystem with it, so the call is refused. Everything a boot volume cannot
do — [extend](#growing-a-volume) included — follows from that.

## Growing a volume

<Tabs>
  <Tab title="Console">
    Open the volume and choose **Extend**, or use the **Extend** row action on
    **Storage → Volumes**. The **Extend Volume** dialog shows **Current Size**
    and takes a **New Size (GB)**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/volumes/{volume_id}/extend
    { "new_size_gb": 200 }
    ```
  </Tab>
</Tabs>

The call answers **`202`** with the volume in `extending` and returns to
`available` at the new size. `new_size_gb` must be strictly greater than the
current size; anything else is `400 VOLUME_SIZE_INVALID`.

<Warning>
  **A volume cannot shrink.** There is no call that reduces `size_gb`, and the
  quota you committed at the larger size stays committed. Grow in the steps you
  actually need.
</Warning>

<Warning>
  **Extend only accepts a detached volume.** An extend against a volume in
  `in_use` is `409 VOLUME_NOT_AVAILABLE`. The resize itself does not need the
  volume offline — this is an API-level gate on the volume's state machine, not
  a storage limitation — but today it means: detach, extend, re-attach.
</Warning>

<Warning>
  **A boot volume cannot be extended.** It is permanently `in_use` — detaching
  the boot disk is refused, because it would take the guest's root filesystem
  with it — so an extend can never be accepted on one. Size the boot volume at
  instance creation, or keep growable data on a separate volume.
</Warning>

Growing the volume does not grow the filesystem inside it. Once the volume is
bigger the guest sees the new size on its next device rescan, and expanding the
partition and filesystem is yours to do in the guest.

## Deleting a volume

`DELETE /v1/volumes/{volume_id}` answers **`202`**, flips the row to `deleting`
and tears the volume down asynchronously. Poll until the volume disappears.

Two refusals to know about:

<AccordionGroup>
  <Accordion title="409 VOLUME_IN_USE" icon="link">
    The volume is attached. Detach it from the instance first. Delete is also
    refused during the transient states — `creating`, `extending`, `deleting` —
    so a second operation cannot race the first.
  </Accordion>

  <Accordion title="409 VOLUME_HAS_SNAPSHOTS" icon="camera">
    A volume that still has snapshots will not delete, because those snapshots
    are children of the volume's data and removing the parent underneath them
    is not possible. Delete the snapshots first, then the volume.

    This is a clean refusal rather than a partial teardown, so nothing is lost
    by trying.
  </Accordion>
</AccordionGroup>

Deleting a volume also removes its [snapshot policy](#snapshot-policies) — the
schedule has nothing left to snapshot. Your quota is released once the teardown
finishes, not when the call is accepted, so capacity and your reported usage
stay aligned while a delete is in flight.

## Snapshots

A snapshot is a point-in-time copy of one volume:

<Tabs>
  <Tab title="Console">
    Go to **Storage → Snapshots** and choose **Create Snapshot**, then pick the
    **Volume** and give it a **Name**. The volume's own page has a **Create
    Snapshot** button that opens the same form with that volume already
    selected; it is disabled unless the volume is `available` or `in_use`.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/snapshots
    { "volume_id": "5f8d2c1a-...", "name": "pre-upgrade" }
    ```
  </Tab>
</Tabs>

<Steps>
  <Step title="Take it">
    Allowed against a volume in `available` **or** `in_use` — you do not have
    to detach to snapshot. The snapshot is created asynchronously (`202`,
    status `creating`).
  </Step>

  <Step title="Wait for available">
    Poll `GET /v1/snapshots/{snapshot_id}`. `available` means the snapshot is
    complete and can be restored from; `error` puts the reason in
    `error_message`.
  </Step>
</Steps>

A snapshot name is unique **per volume**, so `nightly` on two different volumes
is fine. `size_gb` is the volume's size frozen at the moment the snapshot was
taken — the volume may have been extended since, so do not read it as the
volume's current size, and do not read it as the space the snapshot occupies.

<Warning>
  Snapshotting an attached volume captures the device as it is at that instant,
  including anything the guest has buffered but not yet flushed. For a database
  or anything else with in-memory state, quiesce or flush inside the guest
  before taking the snapshot if you need it to be application-consistent.
</Warning>

### Restoring from a snapshot

Restoring means creating a **new** volume from the snapshot. There is no
in-place rollback:

<Tabs>
  <Tab title="Console">
    On **Create Volume**, switch **Source** to **From snapshot** and choose the
    **Snapshot**. **Size (GB)** must be at least the snapshot's size, and the
    **Tier** is yours to pick — a restore is not tied to the tier the source
    volume was on.

    **Storage → Snapshots** also has a **Create volume from snapshot** row
    action that opens the same form with the snapshot filled in.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/volumes
    {
      "name": "app-data-restored",
      "volume_type": "ssd",
      "size_gb": 200,
      "source_snapshot_id": "7b1e9c4d-..."
    }
    ```
  </Tab>
</Tabs>

The snapshot has to be `available`, and `size_gb` must be at least the
snapshot's frozen size — you can restore onto a larger volume, never a smaller
one. The restored volume records `source_snapshot_id`, which is also what stops
retention from reaping a snapshot something still depends on.

## Snapshot policies

A policy is a schedule attached to one volume: take a snapshot every
`interval_minutes`, then keep at most `retention_count` of the snapshots that
policy created.

<Tabs>
  <Tab title="Console">
    A schedule belongs to one volume, so it lives on that volume rather than in
    a list of its own. Open the volume, go to its **Settings** tab, and fill in
    the **Snapshot schedule** card: **Name**, **Every (minutes)**, **Keep**, and
    optionally **Also delete after (days)**. Choose **Attach schedule**.

    Once a schedule exists the card gains a switch that pauses it, and its
    buttons become **Save** and **Remove schedule**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/snapshot-policies
    {
      "volume_id": "5f8d2c1a-...",
      "name": "nightly",
      "interval_minutes": 1440,
      "retention_count": 7,
      "retention_days": 30
    }
    ```
  </Tab>
</Tabs>

<ResponseField name="volume_id" type="one policy per volume" required>
  Attaching a second policy to the same volume is a `409`. Update the existing
  one instead.
</ResponseField>

<ResponseField name="interval_minutes" type="1–43200" required>
  A **minimum gap**, not an exact cadence. A snapshot lands at or after the
  interval and never before, and can land a minute or two late when the
  scheduling pass is busy. The floor is one minute because that is how often
  the pass evaluates schedules; the ceiling is 30 days.
</ResponseField>

<ResponseField name="retention_count" type="1–256" required>
  How many of this policy's snapshots to keep. When a run takes the count past
  this, the oldest go first.
</ResponseField>

<ResponseField name="retention_days" type="0–3650, default 0">
  An optional age bound applied **on top of** the count: a snapshot outside
  either window is reaped. `0` means no age bound.
</ResponseField>

<Note>
  The first snapshot lands one interval from now. Attaching a schedule is not
  itself a request for a snapshot — use `POST /v1/snapshots` if you want one
  immediately.
</Note>

Scheduled snapshots are named `<policy>-<UTC timestamp>`, for example
`nightly-20260115T000000Z`, and they inherit the policy's tags so a listing
tells you which schedule produced them without a second lookup.

### What retention will never delete

A schedule is also an automatic deleter, so the limits on what it can remove
matter more than the limits on what it keeps:

<Columns cols={2}>
  <Card title="Snapshots you took by hand" icon="shield">
    Retention only matches snapshots carrying this policy's
    `snapshot_policy_id`. A snapshot a person created has none and is never a
    candidate, whatever tags it has.
  </Card>

  <Card title="Snapshots something depends on" icon="git-branch">
    A snapshot a volume was created from — including a restore still running —
    is skipped and re-examined on a later run. It becomes reapable once the
    dependent volume is gone.
  </Card>

  <Card title="The single newest snapshot" icon="clock">
    The newest snapshot is exempt from the **age** bound. A volume that could
    not be snapshotted for longer than the window never loses its whole
    history that way.
  </Card>

  <Card title="Anything, while paused" icon="pause">
    `enabled: false` pauses the entire policy — no snapshots taken and none
    deleted. A paused schedule that kept reaping would delete history while you
    were looking at it.
  </Card>
</Columns>

<Warning>
  Resuming a paused policy applies the retention window again on its next run.
  If you lowered `retention_count` while it was paused, everything now outside
  the window is reaped on that run.
</Warning>

### Reading a schedule's state

`GET /v1/snapshot-policies/{policy_id}` returns where the policy is in its
cycle:

| Field         | What it tells you                                                               |
| ------------- | ------------------------------------------------------------------------------- |
| `next_run_at` | When the next snapshot is due.                                                  |
| `last_run_at` | When the policy last fired. Absent until the first run.                         |
| `last_error`  | Why the most recent run produced no snapshot. Empty after a run that succeeded. |

`last_error` is the field to check on a schedule that has stopped producing
snapshots — it carries reasons like an exhausted `snapshots` quota, or a volume
that was mid-extend when the window came around. The **Snapshot schedule** card
prints all three, so a stalled schedule can be read without leaving the console.

<Info>
  A missed window costs **one** snapshot, not one per window missed.
  `next_run_at` is re-stamped to `now + interval_minutes` each time the policy
  fires, never to `previous + interval`, so a schedule that could not run for
  six hours takes a single snapshot when it resumes rather than a catch-up
  burst. Changing `interval_minutes` re-bases the next run off now too, so
  shortening a daily schedule to hourly takes effect within the hour.
</Info>

Deleting a policy detaches the schedule and **keeps** every snapshot it already
took — they become ordinary snapshots you own outright and are never reaped
again. Delete the snapshots themselves if that is what you meant.

## Volume statuses

```mermaid theme={null}
stateDiagram-v2
    [*] --> creating: create
    creating --> available: provisioned
    creating --> error: provisioning failed
    available --> in_use: attached to an instance
    in_use --> available: detached
    available --> extending: extend
    extending --> available: resized
    extending --> error: resize failed
    available --> deleting: delete
    error --> deleting: delete
    deleting --> [*]: gone
```

| Status      | Meaning                                                      |
| ----------- | ------------------------------------------------------------ |
| `creating`  | Being provisioned. Not yet attachable.                       |
| `available` | Ready to attach, extend, snapshot or delete.                 |
| `in_use`    | Attached to an instance. Snapshot yes; extend and delete no. |
| `extending` | Resize in flight.                                            |
| `deleting`  | Teardown in progress.                                        |
| `error`     | The last operation failed. Read `error_message`.             |

Snapshots use a shorter set: `creating`, `available`, `deleting`, `error`.

## Quotas

Four storage counters, all scoped to your organization within a region:

| Quota               | Counts                                                   |
| ------------------- | -------------------------------------------------------- |
| `volumes`           | Volume count.                                            |
| `volume_storage_gb` | Sum of `size_gb` across your volumes.                    |
| `snapshots`         | Snapshot count, scheduled and manual alike.              |
| `buckets`           | Buckets — see [object storage](/storage/object-storage). |

<Note>
  Quota is consumed for the lifetime of a row, not just while it is healthy. A
  volume parked in `error` still counts against `volumes` and
  `volume_storage_gb` — which mirrors what you can see — so delete failed
  volumes rather than leaving them.
</Note>

An extend reserves the delta before it starts, so an extend that would cross
`volume_storage_gb` is refused up front rather than half-applied.

## Naming, CRNs and tag-conditioned access

Volume, snapshot and snapshot-policy CRNs are built from the **name**, not the
id, and carry the region and your account:

```
crn:storage:sa-saopaulo-1:my-account:volume/app-data-01
crn:storage:sa-saopaulo-1:my-account:snapshot/pre-upgrade
crn:storage:sa-saopaulo-1:my-account:snapshot-policy/nightly
```

Because the CRN is name-based, an [IAM policy](/iam/policies) can gate a naming
convention directly — `crn:storage:*:my-account:volume/prod-*` covers both the
volumes that exist and the ones that will.

All three resources accept a `tags` map, and the tag keys are policy context.
`basalt:ResourceTag/<key>` fences access to volumes already labelled a certain
way; `basalt:RequestTag/<key>` fences what a caller may label a volume *as*,
which is what makes a create authorizable before the volume exists:

```json theme={null}
{
  "sid": "ProductionVolumesOnly",
  "effect": "allow",
  "actions": ["storage:CreateVolume", "storage:ExtendVolume"],
  "resources": ["crn:storage:*:my-account:volume/*"],
  "conditions": [
    { "operator": "equals", "key": "basalt:RequestTag/env", "values": ["production"] }
  ]
}
```

## Listing and pagination

Volumes, snapshots and policies are listed newest-first and paginated with an
opaque cursor. Pass `marker` from the previous page's `meta` to get the next
one:

```bash theme={null}
GET /v1/volumes?limit=50&marker=<meta.marker>
```

<Warning>
  Do not infer the end of a list from a short page. Read `meta.has_more`, and
  keep passing `meta.marker` until it is false — a page can come back smaller
  than the `limit` you asked for.
</Warning>

Volume and snapshot listings also take `name` (case-insensitive substring) and
`status`; snapshots take `volume_id` to narrow to one volume, and policies take
`volume_id` and `enabled`.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Create rejected the volume type" icon="triangle-alert">
    `hdd` is a real tier but not a creatable one — its pool is reserved for
    cold object storage. Use `ssd` or `nvme`. `GET /v1/volume-types` always
    lists exactly what you may create on.
  </Accordion>

  <Accordion title="Extend returns 409 VOLUME_NOT_AVAILABLE" icon="expand">
    The volume is not in `available`. Attached volumes (`in_use`) are the
    common case — detach, extend, re-attach. A volume mid-`creating`,
    `extending` or `deleting` also refuses, and settles on its own.

    A boot volume can never reach `available` while its instance exists, so an
    extend on one has no path to success. Move the data onto a separate volume,
    or rebuild the instance with a larger boot volume.
  </Accordion>

  <Accordion title="Delete returns 409 VOLUME_HAS_SNAPSHOTS" icon="camera">
    List them with `GET /v1/snapshots?volume_id=<id>` and delete them first. If
    a snapshot policy is still attached it may be creating new ones behind you
    — delete the policy, or pause it with `enabled: false`, before clearing the
    backlog.
  </Accordion>

  <Accordion title="A snapshot policy has stopped taking snapshots" icon="calendar-clock">
    Read `last_error` on the policy. The usual causes are an exhausted
    `snapshots` quota — clear space by lowering `retention_count` or deleting
    old snapshots — and a volume that was in a transient state when the window
    came around, which resolves itself on the next run.

    A policy with `enabled: false` produces nothing at all, and `next_run_at`
    tells you whether the schedule believes it is due.
  </Accordion>

  <Accordion title="A restored volume came back the wrong size" icon="hard-drive">
    A restore clones the snapshot and then grows to the `size_gb` you asked
    for, so it is never smaller than the snapshot's frozen size. The
    filesystem inside it is still the size it was when the snapshot was taken —
    expand it in the guest.
  </Accordion>

  <Accordion title="Status is error" icon="circle-alert">
    `error_message` carries the last failure. The row keeps its quota while it
    exists, so delete a volume or snapshot you do not intend to retry.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Object storage" icon="boxes" href="/storage/object-storage">
    Buckets, and the S3-compatible endpoint that serves them.
  </Card>

  <Card title="Compute" icon="server" href="/compute">
    Attaching volumes, boot volumes and device slots.
  </Card>

  <Card title="Writing policies" icon="shield" href="/iam/policies">
    Tag conditions and CRN patterns for storage resources.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every storage operation, with request and response schemas.
  </Card>
</CardGroup>
