> ## 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.

# Volumes

> Creating a volume, the types on offer, attaching it to an instance, growing it, and deleting it.

## 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>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage volume create \
      --name app-data-01 --volume-type ssd --size-gb 100 \
      --tags env=production
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    cfg, err := basaltic.NewConfig(ctx,
        basaltic.WithClientCredentials(os.Getenv("BASALTIC_ACCESS_KEY_ID"), os.Getenv("BASALTIC_SECRET_ACCESS_KEY")),
        basaltic.WithRegion("sa-saopaulo-1"),
    )
    if err != nil {
        log.Fatal(err)
    }

    vol, err := storage.New(cfg).CreateVolume(ctx, &storage.VolumeCreateRequest{
        Name:       "app-data-01",
        VolumeType: "ssd",
        SizeGB:     100,
        Tags:       storage.Tags{"env": "production"},
    })
    ```

    The call returns with the volume in `creating`; poll `GetVolume` until it
    reaches `available`.
  </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. `GET /v1/volume-types` lists exactly what you may create on.
</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>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic compute instance attach-volume <instance-id> --volume-id <volume-id>
    ```

    `--device`, `--mount-path` and `--fstype` are optional and behave as the
    console's fields do.
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    att, err := compute.New(cfg).AttachInstanceVolume(ctx, instanceID,
        &compute.AttachInstanceVolumeRequest{
            VolumeID: volumeID,
        })
    ```

    Attachment lives on the compute client, not storage — the device binding
    and boot order belong to the instance.
  </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>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage volume extend <volume-id> --new-size-gb 200
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    vol, err := storage.New(cfg).ExtendVolume(ctx, volumeID, &storage.VolumeExtendRequest{
        NewSizeGB: 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.
