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.
| 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 |
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 125gp3 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 tableThis is the single easiest cost-and-performance improvement available in most AWS accounts, and it is frequently untouched because nothing surfaces it.
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 MaximumSnapshots 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 volumeAutomate 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
}]
}'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-0123456789abcdef0Fast 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.
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 / # xfsForgetting 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.
aws ec2 enable-ebs-encryption-by-default
aws ec2 modify-ebs-default-kms-key-id --kms-key-id alias/ebsSet 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 gp3That requires downtime for the swap, which is the argument for enabling encryption by default before you have volumes to migrate.
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-1aThis 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.
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.
| 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 |
EbsOptimizedInfo before provisioning IOPS.growpart and resize2fs or xfs_growfs.<MCQExercise exercise={{ id: 'aws-ebs-quiz1', title: 'Provisioned IOPS that changed nothing', description: 'A gp3 volume is raised from 3,000 to 12,000 IOPS on an m6i.large instance. Database performance does not improve measurably. Why?', options: [ { id: 'a', text: 'The instance type caps EBS performance: an m6i.large sustains roughly 3,600 IOPS, so the extra provisioned IOPS cannot be used' }, { id: 'b', text: 'gp3 volumes cannot exceed 3,000 IOPS regardless of what is provisioned' }, { id: 'c', text: 'The volume needs to be detached and reattached for the new IOPS to apply' }, { id: 'd', text: 'The filesystem must be extended before additional IOPS become available' }, ], correctId: 'a', explanation: 'Every instance type has its own EBS bandwidth and IOPS limits, and the effective performance is the lower of the volume and the instance. An m6i.large has a baseline around 3,600 IOPS and 81 MiB/s, so provisioning 12,000 on the volume simply costs more with no benefit. Check describe-instance-types with the EbsOptimizedInfo query before provisioning, and note the difference between baseline and maximum. Smaller instances burst their EBS bandwidth, so sustained workloads should be planned against baseline.', wrongExplanations: { b: 'gp3 supports up to 16,000 IOPS; 3,000 is the included baseline rather than a ceiling.', c: 'modify-volume applies online without detaching, and the change was applied successfully.', d: 'Filesystem size relates to capacity, not to IOPS.', }, }} />
<MCQExercise exercise={{ id: 'aws-ebs-quiz2', title: 'A disk still full after resizing', description: '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?', options: [ { id: 'a', text: 'The filesystem has to be extended separately. Growpart for the partition, then resize2fs or xfs_growfs' }, { id: 'b', text: 'The instance must be stopped and started for the new size to register' }, { id: 'c', text: 'The volume needs converting to gp3 before sizes above 100 GiB take effect' }, { id: 'd', text: 'A new snapshot must be taken and restored at the larger size' }, ], correctId: 'a', explanation: 'Growing a volume changes the block device, and the filesystem on it has no knowledge of the extra space until told. lsblk shows the device at the new size while df shows the filesystem at the old one. Extend the partition with growpart if the device is partitioned, then grow the filesystem. Resize2fs for ext4 or xfs_growfs -d for xfs. Both work on a mounted filesystem, so no downtime is needed. Note also the six-hour cooldown before the same volume can be modified again.', wrongExplanations: { b: 'Volume resizing is online and a reboot does not extend a filesystem.', c: 'Volume type has no bearing on size, and gp2 supports far larger volumes than 100 GiB.', d: 'Snapshot and restore would work but is a far more disruptive route to the same outcome.', }, }} />
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.
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.
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.
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.
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.
Choose between Kinesis Data Streams, Firehose, SQS and MSK. Learn shards, hot partition keys, consumer lag, enhanced fan-out and how to reshard a busy stream.
Query S3 with SQL using Athena and keep the bill low. Learn partitioning, partition projection, why Parquet beats JSON and how to cap bytes scanned per query.
Run blue-green and canary deployments to EC2, Lambda and ECS with CodeDeploy. Write the appspec file, add lifecycle hooks and roll back on CloudWatch alarms.