AWS EBS Deep Dive
Choose and tune block storage. Gp3 over gp2, io2 Block Express, why instance type caps your volume, snapshots and fast restore, resizing safely, and encryption.
EBS Deep Dive
EBS provides block storage for EC2. Volumes that behave like disks, persist independently of the instance, and are replicated within a single availability zone.
Two things account for most EBS disappointment: still using gp2, and not realising that the instance type caps what any volume can deliver.
Volume types
| Type | Max IOPS | Max throughput | Use |
|---|---|---|---|
| gp3 | 16,000 | 1,000 MiB/s | Default for everything |
| gp2 | 16,000 (size-tied) | 250 MiB/s | Legacy (migrate) |
| io2 Block Express | 256,000 | 4,000 MiB/s | Large databases |
| st1 | 500 | 500 MiB/s | Sequential (logs, big data) |
| sc1 | 250 | 250 MiB/s | Cold, infrequent |
Migrate off gp2
gp2's performance is tied to size: 3 IOPS per GiB, so 3,000 IOPS requires a 1,000 GiB volume whether or not you need the space. Below 1,000 GiB it relies on burst credits, which behave exactly like EFS bursting. Fine until they run out, then abruptly slow.
gp3 decouples the two. Every gp3 volume starts at 3,000 IOPS and 125 MiB/s regardless of size, and you provision more independently.
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 \
--volume-type gp3 --iops 3000 --throughput 125
gp3 is also about 20% cheaper per GiB than gp2, and the conversion happens online with no downtime and no snapshot. A 100 GiB gp2 volume delivers 300 baseline IOPS; the same volume as gp3 delivers 3,000 for less money.
Find them all:
aws ec2 describe-volumes --filters Name=volume-type,Values=gp2 \
--query 'Volumes[].[VolumeId,Size,State,Attachments[0].InstanceId]' --output table
This is the single easiest cost-and-performance improvement available in most AWS accounts, and it is frequently untouched because nothing surfaces it.
The instance type is the real limit
Provisioning 16,000 IOPS on a volume attached to an instance that supports 6,000 achieves nothing, and nothing warns you.
aws ec2 describe-instance-types --instance-types m6i.large \
--query 'InstanceTypes[0].EbsInfo.EbsOptimizedInfo' --output table
-------------------------------------------------
| BaselineIops | 3600 |
| BaselineThroughputInMBps | 81.25 |
| MaximumIops | 20000 |
| MaximumThroughputInMBps | 593.75 |
-------------------------------------------------
Note baseline versus maximum: smaller instances burst their EBS bandwidth too, so sustained performance is the baseline figure. An m6i.large sustains about 81 MiB/s regardless of how the volume is configured.
Check this before provisioning IOPS. Paying for 10,000 IOPS on an instance that sustains 3,600 is pure waste, and "the volume is fast enough but the application is slow" is usually this.
# What is actually being used
aws cloudwatch get-metric-statistics --namespace AWS/EBS \
--metric-name VolumeReadOps --dimensions Name=VolumeId,Value=vol-0123456789abcdef0 \
--start-time 2026-09-15T00:00:00Z --end-time 2026-09-16T00:00:00Z \
--period 300 --statistics Average Maximum
Snapshots
Snapshots are incremental and stored in S3. The first captures every used block; later ones store only changes, though each remains independently restorable. Deleting an old snapshot never breaks a newer one.
aws ec2 create-snapshot --volume-id vol-0123456789abcdef0 \
--description "pre-migration $(date -I)" \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=Retain,Value=30d}]'
For a multi-volume instance, snapshot them together so they are mutually consistent:
aws ec2 create-snapshots --instance-specification 'InstanceId=i-0123456789abcdef0' \
--description "consistent set" --copy-tags-from-source volume
Automate retention with Data Lifecycle Manager rather than a cron job:
aws dlm create-lifecycle-policy --description "daily 7d" --state ENABLED \
--execution-role-arn "$DLM_ROLE_ARN" \
--policy-details '{
"ResourceTypes": ["VOLUME"],
"TargetTags": [{"Key": "Backup", "Value": "daily"}],
"Schedules": [{
"Name": "daily",
"CreateRule": {"Interval": 24, "IntervalUnit": "HOURS", "Times": ["03:00"]},
"RetainRule": {"Count": 7},
"CopyTags": true
}]
}'
Restored volumes are slow at first
The trap that catches people during a recovery. A volume created from a snapshot is available immediately, but blocks are fetched from S3 on first access, so early reads are dramatically slower than normal, exactly when you are trying to get a service back.
aws ec2 enable-fast-snapshot-restore \
--availability-zones eu-west-1a --source-snapshot-ids snap-0123456789abcdef0
Fast snapshot restore removes the penalty at an hourly cost per snapshot per AZ. Enable it on the snapshots your recovery plan depends on, not all of them, and if you cannot, pre-warm by reading the volume through before putting it in service.
Resizing
Volumes grow online, and cannot shrink:
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 --size 200
aws ec2 describe-volumes-modifications --volume-id vol-0123456789abcdef0 \
--query 'VolumesModifications[0].[ModificationState,Progress]'
The filesystem does not grow by itself. The volume is bigger and df shows the old size until you extend it:
lsblk
sudo growpart /dev/nvme0n1 1 # if partitioned
sudo resize2fs /dev/nvme0n1p1 # ext4
sudo xfs_growfs -d / # xfs
Forgetting this step is why "I resized the volume and the disk is still full" is such a common report.
A volume cannot be modified again for six hours after a change, which is worth knowing before an emergency resize.
Encryption
aws ec2 enable-ebs-encryption-by-default
aws ec2 modify-ebs-default-kms-key-id --kms-key-id alias/ebs
Set this in every account and region. It costs nothing and removes the possibility of an unencrypted volume being created by accident.
An existing unencrypted volume cannot be encrypted in place. The path is snapshot, copy the snapshot with encryption, create a new volume, swap it in:
SNAP=$(aws ec2 create-snapshot --volume-id vol-unencrypted --query SnapshotId --output text)
aws ec2 wait snapshot-completed --snapshot-ids "$SNAP"
ENC=$(aws ec2 copy-snapshot --source-region eu-west-1 --source-snapshot-id "$SNAP" \
--encrypted --kms-key-id alias/ebs --query SnapshotId --output text)
aws ec2 create-volume --snapshot-id "$ENC" --availability-zone eu-west-1a --volume-type gp3
That requires downtime for the swap, which is the argument for enabling encryption by default before you have volumes to migrate.
Multi-attach
io1 and io2 volumes can attach to up to 16 instances in the same availability zone:
aws ec2 create-volume --volume-type io2 --size 100 --iops 10000 \
--multi-attach-enabled --availability-zone eu-west-1a
This does not give you a shared filesystem. Two instances mounting ext4 on the same volume will corrupt it, because neither knows about the other's writes. It requires a cluster-aware filesystem such as GFS2 or an application managing its own locking.
If you want shared file storage, that is EFS.
Common errors
VolumeInUse: vol-0123456789abcdef0 is already attached to an instance
Detach first, or use multi-attach if the volume type and filesystem support it.
InvalidParameterValue: Iops to volume size ratio of 60.000 is too high;
maximum is 50.000 IOPS per GiB
gp3 caps at 500 IOPS per GiB, io2 at 500 as well within its own limits. Increase the size or lower the IOPS.
IncorrectModificationState: Volume modification in progress
Six-hour cooldown between modifications.
df shows the old size after resizing. Extend the filesystem.
A snapshot restore is slow. Blocks are being lazily loaded. Fast snapshot restore, or pre-warm.
Quick reference
| Task | Command |
|---|---|
| Find gp2 volumes | describe-volumes --filters Name=volume-type,Values=gp2 |
| Convert to gp3 | modify-volume --volume-type gp3 --iops 3000 --throughput 125 |
| Instance EBS limits | describe-instance-types --query '…EbsOptimizedInfo' |
| Snapshot | aws ec2 create-snapshot --volume-id <v> |
| Consistent multi-volume | aws ec2 create-snapshots --instance-specification … |
| Automate backups | aws dlm create-lifecycle-policy |
| Fast restore | aws ec2 enable-fast-snapshot-restore |
| Resize | modify-volume --size N then resize2fs or xfs_growfs |
| Encrypt by default | aws ec2 enable-ebs-encryption-by-default |
Key takeaways
- Convert every gp2 volume to gp3. Better baseline performance, about 20% cheaper, no downtime.
- The instance type caps volume performance: check
EbsOptimizedInfobefore provisioning IOPS. - Smaller instances burst EBS bandwidth too, so plan against the baseline figure.
- Snapshots are incremental but independently restorable. Deleting an old one is safe.
- A restored volume is slow until blocks load. Use fast snapshot restore where recovery time matters.
- Resizing does not extend the filesystem. Run
growpartandresize2fsorxfs_growfs. - Six hours between volume modifications.
- Enable encryption by default; existing volumes need a snapshot-copy-swap.
- Multi-attach is not shared storage. That is EFS.
Check your understanding
Provisioned IOPS that changed nothing
A gp3 volume is raised from 3,000 to 12,000 IOPS on an m6i.large instance. Database performance does not improve measurably. Why?
A disk still full after resizing
A volume was resized from 50 GiB to 200 GiB and the modification shows as completed. df -h still reports 50G with no free space. What is missing?
Frequently Asked Questions
Should I use gp2 or gp3?
gp3, without qualification. gp2 ties IOPS to volume size at 3 per GiB and relies on burst credits below 1,000 GiB, so a 100 GiB volume has a 300 IOPS baseline. Every gp3 volume starts at 3,000 IOPS and 125 MiB/s regardless of size, lets you provision more independently, and costs roughly 20% less per GiB. Conversion is a single modify-volume call, happens online with no downtime, and needs no snapshot. Auditing an account for gp2 volumes is usually the cheapest performance win available.
Why doesn't increasing IOPS improve performance?
Because the instance type has its own EBS limits and the effective figure is the lower of the two. An m6i.large sustains around 3,600 IOPS and 81 MiB/s, so provisioning 12,000 on the volume adds cost and nothing else. Check describe-instance-types with the EbsInfo.EbsOptimizedInfo query, and pay attention to baseline versus maximum. Smaller instances burst their EBS bandwidth, so sustained workloads should be sized against the baseline number.
How do EBS snapshots work and is deleting old ones safe?
Snapshots are incremental: the first stores every used block, subsequent ones store only changed blocks, and all of them reference a shared set of blocks in S3. Each snapshot is independently restorable, and deleting one never invalidates another. AWS retains any blocks still needed by remaining snapshots. So a retention policy that keeps the last seven daily snapshots works exactly as expected. Use Data Lifecycle Manager to automate creation and expiry rather than scripting it.
Why is a volume restored from a snapshot slow?
Because blocks are pulled from S3 lazily, on first access, so a volume is usable immediately but reads of not-yet-loaded blocks are much slower than normal, which lands precisely during a recovery. Fast snapshot restore eliminates the penalty for an hourly charge per snapshot per availability zone; enable it on the snapshots your recovery plan actually depends on. Without it, read the volume through with dd or fio before putting it into service.
How do I resize an EBS volume?
aws ec2 modify-volume --size N grows it online, and volumes cannot shrink. The step people miss is that the filesystem does not grow with the device: run growpart if the device is partitioned, then resize2fs for ext4 or xfs_growfs -d for xfs, both of which work on a mounted filesystem. lsblk shows the device size and df the filesystem size, so comparing them confirms which half is outstanding. A volume cannot be modified again for six hours.
Can I encrypt an existing unencrypted EBS volume?
Not in place. Take a snapshot, copy the snapshot with --encrypted and your KMS key, create a volume from the encrypted copy, then detach the old volume and attach the new one, which means downtime for the swap. That is the argument for running enable-ebs-encryption-by-default in every account and region now: it costs nothing, prevents unencrypted volumes being created accidentally, and saves you this migration entirely.