Skip to main content
emnode / learn
Cost

Migrate EBS volumes from io1 to io2

Same price per GB and per IOPS, 100x better durability, and cheaper at high IOPS — io2 is the rare free upgrade with an online migration path.

12 min·10 sections·AWS

Last reviewed

io1 vs io2: the basics

What does it mean to migrate a provisioned-IOPS volume from io1 to io2?

io1 is AWS's original provisioned-IOPS SSD — the volume type you reach for when a database or transactional workload needs a guaranteed IOPS floor rather than the best-effort baseline of gp2/gp3. You pay two meters: roughly $0.125 per GB-month for capacity plus roughly $0.065 per provisioned IOPS-month, and you choose the IOPS independently of size up to a 50:1 IOPS-to-GB ratio and a 64,000 IOPS ceiling. It's reliable, but its durability is rated at 99.8–99.9% annually — meaning a small but non-zero fraction of volumes are expected to fail in a given year.

io2 is the successor AWS shipped in 2020, and it is priced identically to io1 in most regions: same ~$0.125/GB-month, same ~$0.065 per provisioned IOPS at the base tier. What you get for the same money is 99.999% annual durability — a 100x improvement, putting a single io2 volume roughly on par with a RAID-mirrored io1 pair — plus a higher 500:1 IOPS-to-GB ratio. io2 Block Express extends the envelope much further: up to 256,000 IOPS, 4,000 MiB/s throughput, and 64 TiB volumes for workloads that outgrow standard limits.

Cost Optimization Hub and most FinOps scanners flag every remaining io1 volume as a migration candidate, because the upgrade is free at low IOPS and actually cheaper at high IOPS. io2 uses tiered IOPS pricing: the per-IOPS rate drops in the bands above 32,000 and again above 64,000 provisioned IOPS, so a high-IOPS io2 volume costs less than the same io1 volume while never costing more at the low end. There's no commitment to break and no snapshot to take — the migration is an online modify-volume call.

In this lesson you'll learn why io2 is a same-price-or-cheaper upgrade over io1, how the tiered IOPS pricing makes high-IOPS volumes cost less, and how to migrate a live fleet without downtime. You'll see the modify-volume API at work, the optimizing state, the 6-hour cooldown, and the prerequisites (instance-type support, Block Express) that gate the handful of volumes you can't flip blindly.

Fun fact

One io2 volume replaces a mirrored io1 pair

io1's 99.9% annual durability sounds reassuring until you do the arithmetic at fleet scale: at 99.9%, you should statistically expect about one in a thousand volumes to fail per year. Teams used to buy their way around this by mirroring two io1 volumes in software RAID — doubling both the GB and the provisioned-IOPS bill — to reach the reliability a single io2 now delivers at 99.999%. AWS effectively gave away the cost of that second volume: migrate to io2 and you can often retire the mirror entirely, halving the spend on those workloads while improving durability rather than just matching it.

io1 → io2 in action

Nina runs cloud platform at a fintech with a fleet of provisioned-IOPS databases. A FinOps review flags $6,200/month sitting in io1 volumes — a mix of 500 GB OLTP volumes at 16,000 IOPS and a pair of analytics monsters provisioned at 60,000 IOPS each, all created during a 2021 migration and never revisited.

She pulls the inventory. The OLTP volumes flip to io2 at zero cost change — same GB rate, same IOPS rate — but gain the 100x durability bump, which lets her retire the software-RAID mirror on two of them. The 60,000-IOPS analytics volumes are the real prize: on io2's tiered pricing, the IOPS above 32,000 bill at a lower rate, so each one drops by roughly $400/month just by changing the volume type.

She validates on one staging volume — modify-volume --volume-type io2, watch it move modifying → optimizing → completed while the workload keeps serving — then sweeps the fleet. Projected monthly savings from the high-IOPS tiering plus the retired mirrors: ~$1,500, recurring, with durability strictly improved and not a second of downtime.

First, list every io1 volume in the region with its size and provisioned IOPS so we can spot the high-IOPS volumes that will get cheaper on io2.

$ aws ec2 describe-volumes --filters Name=volume-type,Values=io1 --query 'Volumes[].[VolumeId,Size,Iops,State]' --output table
-------------------------------------------------------
| DescribeVolumes |
+-----------------------+------+-------+--------------+
| vol-0a1b2c3d4e5f60001 | 500 | 16000 | in-use |
| vol-0a1b2c3d4e5f60002 | 500 | 16000 | in-use |
| vol-0a1b2c3d4e5f60003 | 2000 | 60000 | in-use |
| vol-0a1b2c3d4e5f60004 | 2000 | 60000 | in-use |
+-----------------------+------+-------+--------------+
# The two 60,000-IOPS volumes cross the 32k tier — they get cheaper on io2, not just more durable.

Inventory pass — io1 volumes with their provisioned IOPS. Anything above 32,000 IOPS saves money on the io2 tier.

Now flip one volume to io2. The call returns immediately; the volume keeps serving traffic the entire time, IOPS and size preserved.

$ aws ec2 modify-volume --volume-id vol-0a1b2c3d4e5f60003 --volume-type io2
{
"VolumeModification": {
"VolumeId": "vol-0a1b2c3d4e5f60003",
"ModificationState": "modifying",
"OriginalVolumeType": "io1",
"OriginalIops": 60000,
"TargetVolumeType": "io2",
"TargetIops": 60000,
"TargetSize": 2000,
"Progress": 0
}
}
# Billing flips to io2 immediately — durability jumps to 99.999% and the IOPS above 32k now bill at the lower tier.

Live volume modification — no detach, no snapshot, no reboot. Size and provisioned IOPS carry over unchanged.

What happens at the API layerdeep dive

ModifyVolume is an asynchronous, online operation on the EBS data plane, and io1 → io2 is the gentlest kind of modification it handles — the data already lives on the same SSD substrate, so AWS doesn't move a byte. What changes is the metering record and the durability contract the storage layer enforces. The call returns in under a second, the volume stays attached and writable, and pricing flips to io2 the moment the request is accepted, not when it finishes. The volume passes through modifying, then optimizing (usually brief for a like-for-like type change), then completed.

The pricing detail that makes this more than a durability play is io2's tiered IOPS rate. On io1 every provisioned IOPS bills at a flat $0.065. On io2 the first 32,000 IOPS bill at ~$0.065, the band from 32,001 to 64,000 IOPS bills at a lower rate ($0.046), and anything above 64,000 (Block Express territory) bills lower still (~$0.032). Because the GB rate and the first-tier IOPS rate match io1 exactly, io2 is never more expensive at the low end and is strictly cheaper for any volume provisioned above 32,000 IOPS.

Two constraints gate the sweep. First, EBS rate-limits modifications to one per volume per six hours, so validate on a single volume before the fleet run — you can't immediately undo a fat-fingered change. Second, io2 attaches to most current instance types out of the box, but io2 Block Express (and its higher IOPS/size limits) requires a supported Nitro instance type; a few older instance families don't support io2 at all and need an instance migration first. Check describe-instance-types before sweeping a mixed-age fleet.

# Track the migration state for a volume that's currently optimizing.
aws ec2 describe-volumes-modifications \
  --volume-ids vol-0a1b2c3d4e5f60003 \
  --query 'VolumesModifications[0].[ModificationState,Progress,TargetVolumeType,TargetIops,StartTime]' \
  --output table

# Sweep the fleet — io1 -> io2 preserving each volume's existing provisioned IOPS.
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=io1 \
  --query 'Volumes[].VolumeId' --output text \
  | tr '\t' '\n' \
  | xargs -n1 -P4 -I{} aws ec2 modify-volume --volume-id {} --volume-type io2

What's the impact of staying on io1?

The direct durability hit is the headline. At io1's 99.9% annual durability you expect roughly 1 in 1,000 volumes to fail per year; at io2's 99.999% it's 1 in 100,000 — a 100x reduction in expected failures for the exact same monthly charge. On the workloads that run on provisioned IOPS — production databases, transaction logs, latency-sensitive stores — a volume failure isn't a rounding error, it's an incident with a restore, a possible data-loss window, and a postmortem. Staying on io1 means paying full premium price for materially worse odds on your most critical storage.

The direct cost hit shows up on high-IOPS volumes. A 2 TB io1 volume at 60,000 provisioned IOPS bills roughly $250 for capacity plus ~$3,900 for IOPS at the flat io1 rate. The same volume on io2 keeps the capacity charge but the 28,000 IOPS above the 32,000 threshold drop to the lower tier — saving on the order of $400/month per volume, recurring, for changing one field. Multiply across a fleet of analytics or high-throughput database volumes and the tiering alone funds the migration many times over.

There's a structural cost too: redundant mirroring. Teams that needed better-than-io1 durability historically ran two io1 volumes in software RAID-1, doubling both the GB and the provisioned-IOPS bill. A single io2 volume meets or beats that durability target, so the mirror can be retired — halving the spend on those workloads while improving reliability. Many io1 fleets carry this hidden 2x premium without anyone remembering why.

Finally, io1 is the older generation AWS is no longer investing in. io2 and io2 Block Express get the higher IOPS:GB ratios (500:1 vs io1's 50:1), the larger volume sizes, the higher per-volume ceilings, and the new-feature priority. Staying on io1 means staying on the side of provisioned-IOPS EBS that's frozen in time — paying the same money for fewer capabilities and worse durability.

How do you migrate safely?

The mechanics are trivial; the discipline is in inventory, checking the handful of prerequisites, and not blowing past the 6-hour cooldown. Run the four-step loop below and you'll convert any provisioned-IOPS fleet without an outage.

1. Inventory every io1 volume in every region

Use describe-volumes with Name=volume-type,Values=io1 and dump each volume's size, provisioned IOPS, attached instance, and tags. Sort by IOPS: volumes above 32,000 IOPS will get cheaper on io2 (tiered pricing), so quantify that saving first. Flag any unattached io1 volumes — those should be deleted, not migrated — and note any volumes that are halves of a software-RAID mirror, since those mirrors may be retirable once a single io2 volume meets the durability bar.

2. Check instance-type and Block Express prerequisites

io2 attaches to most current instance types, but a few older instance families don't support it at all, and io2 Block Express (needed only if you want IOPS/size beyond standard io2 limits) requires a supported Nitro instance type. Cross-reference each io1 volume's attached instance against describe-instance-types. For ordinary like-for-like migrations within standard io2 limits this is almost always a non-issue — but it's the one prerequisite that turns an online change into an instance migration if you skip it.

3. Pilot on one non-critical volume, then sweep

Pick a dev or staging io1 volume, run modify-volume --volume-type io2 against it, and watch it move modifying → optimizing → completed while the attached workload keeps serving I/O. Confirm CloudWatch shows no errors and the IOPS contract is unchanged. Then sweep the fleet, preserving each volume's existing provisioned IOPS, and parallelise with xargs -P while staying under the per-volume rate limit. Remember the 6-hour cooldown: you get one modification per volume per six hours, so get the IOPS right on the way through.

4. Track via DescribeVolumesModifications and verify the cost line

Don't assume the migration finished because the API accepted the call. Poll describe-volumes-modifications until every volume reads completed, then check the next day's Cost Explorer EBS line: the io1 GB-month and IOPS-month figures should fall to zero and the io2 lines rise, with the high-IOPS volumes landing lower than their io1 equivalents. Where you retired RAID mirrors, confirm the second volume is actually deleted, not just detached and still billing.

# Bulk migration preserving each io1 volume's provisioned IOPS.
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=io1 \
  --query 'Volumes[].[VolumeId,Iops]' --output text \
  | while read VOL IOPS; do
      # io2 inherits size automatically; pass IOPS explicitly to avoid any regression.
      aws ec2 modify-volume --volume-id "$VOL" --volume-type io2 --iops "$IOPS"
    done

# Watch the fleet finish optimizing.
aws ec2 describe-volumes-modifications \
  --filters Name=modification-state,Values=optimizing,modifying \
  --query 'VolumesModifications[].[VolumeId,ModificationState,Progress]' \
  --output table

Quick quiz

Question 1 of 5

You have a 2 TB io1 volume on a production database provisioned at 50,000 IOPS, attached to a current-generation Nitro instance. What's the right move?

You've completed Migrate EBS volumes from io1 to io2. You now know the durability story (99.8–99.9% → 99.999%), the tiered-IOPS pricing that makes high-IOPS volumes cheaper, the prerequisites that gate the handful of volumes you can't flip blindly, and the four-step loop — inventory, prerequisite-check, pilot-then-sweep, verify — to migrate a fleet of any size without downtime. The next time a scanner surfaces an io1 finding, you'll know it's a same-day, no-trade-off upgrade.

Back to the library