Migrate gp2 EBS volumes to gp3 and cut storage costs about 20%

There is a line item on your AWS bill that has been quietly overcharging you for years. It is your gp2 EBS volumes. The fix is a one-command change that runs while your instances stay online. Most teams never get to it because it sounds risky and nobody owns the storage tier. Let me walk you through the whole thing.
Why gp3 is almost always cheaper than gp2
gp3 is the newer general-purpose SSD volume type. It lists at about $0.08 per GB-month in most regions. gp2 lists at about $0.10 per GB-month. That is roughly 20% off the same storage, before you change anything else.
The reason the discount is close to free is performance. gp2 ties its baseline IOPS to volume size, at 3 IOPS per GB. gp3 ships a flat baseline of 3000 IOPS and 125 MB/s of throughput at no extra cost, regardless of size.
Walk through a 500 GiB volume. On gp2, 500 GiB times 3 IOPS gives you a 1500 IOPS baseline. On gp3, that same volume starts at 3000 IOPS. You pay less and get twice the baseline performance.
So for the common case, a smaller attached volume serving a database or an app server, gp3 is a clean win. The bill drops and the sustained IOPS floor rises, usually by more than you expect. The next section explains why. The migration is online, which we will get to.
The burst credits nobody counts
The 20% discount is the headline. On a small volume the performance change underneath it is the bigger story, and most migration write-ups skip it.
That 3 IOPS per GiB is what gp2 sustains, with a floor of 100 IOPS. So a 100 GiB volume sustains 300 IOPS. That is a long way from the 3000 most people remember gp2 having.
The 3000 figure is a burst, and it runs on credits. AWS gives every gp2 volume a bucket of 5.4 million I/O credits, and the volume earns 3 more per GiB every second it runs at or below baseline. Go above baseline and the bucket drains at the difference between the IOPS you are using and the IOPS you are entitled to.
Push that 100 GiB volume at 3000 IOPS and the bucket empties in 2000 seconds, a little over half an hour. The volume is then pinned at 300 IOPS until credits come back. Refilling an empty bucket at 300 credits a second takes 18000 seconds, so five hours of near-idle time before you have the burst again.
Here is what gp2 actually sustains at each size, against the 3000 IOPS gp3 includes for free.
| gp2 size | gp2 sustained baseline | Burst at 3000 IOPS lasts | Refill from empty | gp3 sustained |
|---|---|---|---|---|
| 33 GiB or smaller | 100 IOPS | 31 minutes | 15 hours | 3000 IOPS |
| 100 GiB | 300 IOPS | 33 minutes | 5 hours | 3000 IOPS |
| 334 GiB | 1002 IOPS | 45 minutes | 90 minutes | 3000 IOPS |
| 750 GiB | 2250 IOPS | 2 hours | 40 minutes | 3000 IOPS |
| 1000 GiB and larger | 3000 IOPS or more | Baseline already exceeds it | n/a | 3000 IOPS |
AWS states the gp3 side plainly in its EBS volume type documentation: gp3 volumes do not use burst performance, and they sustain their full provisioned IOPS indefinitely. So the small volumes everyone waves through as an easy 20% saving are picking up a much higher sustained IOPS floor on the way past. That 100 GiB volume goes from 300 sustained to 3000. Mention it to whoever owns the database, because it is the half of the change they care about.
Throughput barely moves at the bottom of that table. A gp2 volume of 170 GiB or smaller caps at 128 MB/s against gp3's included 125 MB/s, so you trade 3 MB/s for the sustained IOPS. The 334 GiB and 750 GiB rows are a different story, and the next section prices them out.
A gp2 volume that runs out of credits raises no error. It gets slow, and it gets slow exactly when you are busiest, because being busy is what drained the bucket. Average IOPS on a dashboard reads fine while the volume spends its afternoons throttled to baseline.
BurstBalance is the metric that tells you. It reports the percentage of credits left rather than the raw count.
# GNU date. On macOS: date -u -v-14d +%Y-%m-%dT%H:%M:%SZ
aws cloudwatch get-metric-statistics \
--namespace AWS/EBS \
--metric-name BurstBalance \
--dimensions Name=VolumeId,Value=vol-0123456789abcdef0 \
--start-time "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 3600 \
--statistics Minimum \
--output table
A volume whose minimum dips toward zero has been throttled to baseline, and it is the one to move first. The migration buys it sustained performance on top of the saving. A volume sitting at 100% for the whole fortnight never needed its credits, so it moves for the money alone. After the migration the metric stops reporting for that volume, because a gp3 volume has no bucket to report on.
The 334 GiB throughput cliff
IOPS is the half of this migration everyone checks. Throughput is the half that bites, and its cliff sits at 334 GiB, well below the 1000 GiB line.
gp2 throughput scales with volume size the same way its IOPS does. AWS states the top band plainly in its EBS volume type documentation: "Volumes that are 334 GiB and larger deliver 250 MiB/s". Below that, a volume caps at 128 MB/s up to 170 GiB, and between 171 and 333 GiB it reaches 250 MB/s only while it still has burst credits.
gp3 does no such scaling. Every gp3 volume starts at 125 MB/s, whatever its size.
| gp2 size | gp2 throughput | gp3 at defaults | What you lose |
|---|---|---|---|
| 170 GiB and smaller | 128 MB/s | 125 MB/s | 3 MB/s |
| 171 to 333 GiB | 250 MB/s while credits last | 125 MB/s | up to 125 MB/s |
| 334 GiB and larger | 250 MB/s sustained | 125 MB/s | 125 MB/s |
So a 500 GiB gp2 volume sustains 250 MB/s today. Modify it with nothing but --volume-type gp3 and it sustains 125 MB/s tomorrow. Half the throughput is gone from a volume that sits comfortably inside the pile most write-ups wave through as a safe win. The IOPS floor rises in the same change, so a dashboard tracking IOPS reads the migration as an improvement while your sequential scans and backups take twice as long.
Buying it back costs $5 a month. gp3 charges $0.04 per provisioned MB/s-month above the free 125, so the missing 125 MB/s is a flat $5 on a volume of any size, against $10 of storage saving on that 500 GiB volume. Pass --throughput 250 in the same call and you keep the throughput, keep the higher sustained IOPS, and still bank half the discount.
That flat $5 is the same charge the pricing table below applies to every volume of 334 GiB or larger.
The 1000 GiB trap
The IOPS side has a cliff of its own, and it lands at 1000 GiB.
gp2 hands you 3 IOPS per GB of baseline. At exactly 1000 GiB, that is 3000 IOPS, which is the same number gp3 gives you by default. Above 1000 GiB, gp2's baseline keeps climbing while gp3's default stays parked at 3000.
A 4000 GiB gp2 volume carries a 12000 IOPS baseline. Move it to gp3 with the defaults and you drop to 3000 IOPS. For a busy volume that is a real performance regression, and you will feel it before you see the savings.
The trap has a fix. gp3 lets you provision IOPS and throughput on top of the baseline, and the first 3000 IOPS and 125 MB/s are already included. You buy back only the difference. For a 4000 GiB volume you would provision an extra 9000 IOPS to match the old baseline. The next section prices that out.
Often the volume never used that headroom in the first place. Pull the CloudWatch VolumeReadOps and VolumeWriteOps metrics for the last few weeks before you decide. Size IOPS to what the volume actually does, not to what gp2 happened to give it for free.
What matching the old baseline actually costs
Most write-ups stop at "do the math" and leave you to it. Here is the math.
gp3 charges $0.005 per provisioned IOPS-month above the free 3000, and $0.04 per provisioned MB/s-month above the free 125. Those two prices settle the 1000 GiB question at every volume size.
Take the 4000 GiB volume. On gp2 it costs 4000 times $0.10, so $400 a month, and it carries a 12000 IOPS baseline with 250 MB/s of throughput. Match all of that on gp3 and you pay $320 for the storage, $45 for the extra 9000 IOPS, and $5 for the extra 125 MB/s. Your bill reads $370.
The discount shrank from 20% to 7.5%. It did not invert.
Here is the shape across sizes. Each row matches both the IOPS and the 250 MB/s of throughput that gp2 delivers at that size.
| gp2 size | gp2 IOPS to match | gp2 $/mo | gp3 matched $/mo | Saving |
|---|---|---|---|---|
| 500 GiB | 3000 (burst ceiling) | $50 | $45 | 10% |
| 1000 GiB | 3000 (baseline) | $100 | $85 | 15% |
| 2000 GiB | 6000 (baseline) | $200 | $180 | 10% |
| 4000 GiB | 12000 (baseline) | $400 | $370 | 7.5% |
| 6000 GiB | 16000 (cap reached) | $600 | $550 | 8.3% |
The arithmetic behind the table has three terms, and the third one is easy to drop. Every GiB you move saves $0.02 on storage. Matching gp2's 3 IOPS per GiB costs $0.015 per GiB, and only for the IOPS above gp3's free 3000. Matching gp2's throughput is a flat $5 a month on any volume of 334 GiB or larger, because that is where gp2 delivers 250 MB/s against gp3's included 125.
Leave that flat $5 out and you will miss every row by exactly $5. It is the entire gap between the $40 of storage and the $45 in the 500 GiB row, where gp3's free 3000 IOPS already covers gp2's burst ceiling and the uplift is nothing but throughput.
Storage saves more per GiB than matched IOPS cost, so the gap widens with size and the flat charge matters less the larger the volume. AWS states the conclusion plainly in its own migration guidance: a gp3 volume configured to gp2's performance is always the cheaper of the two. That holds at the tightest point too, a 334 GiB volume, where $33.40 becomes $31.72.
Notice the last row. gp2's baseline stops climbing at 16000 IOPS, which it reaches at 5334 GiB. Past that point the storage discount keeps growing while the IOPS you have to buy back does not, so the saving starts widening again. gp3 provisions up to 80000 IOPS, so no volume is too large to match.
One angle before you provision anything. The cheapest IOPS are the ones you never buy, and the cheapest GiB are the ones you never allocate. If the volume backs a Postgres database, check whether unused indexes are padding it before you size the replacement. If you run GCP as well, the same online migration exists there between pd-ssd and pd-balanced. Azure has the same tier gap between Premium SSD v1 and v2, though that one costs you downtime. The VM has to be deallocated around the SKU change, and Azure converts the disk one way only.
Find your gp2 candidates by hand
Start by listing every gp2 volume in the region. The AWS CLI does this in one call.
aws ec2 describe-volumes \
--filters Name=volume-type,Values=gp2 \
--query "Volumes[].{id:VolumeId,size:Size,az:AvailabilityZone,state:State}" \
--output table
That gives you the inventory. Now split it on 334 GiB and 1000 GiB, because those are the two lines where the decision changes.
# Clean wins: under 334 GiB, gp3 matches or beats gp2 on both IOPS and throughput.
aws ec2 describe-volumes \
--filters Name=volume-type,Values=gp2 \
--query "Volumes[?Size < \`334\`].{id:VolumeId,size:Size,az:AvailabilityZone}" \
--output table
# Add --throughput 250: from 334 GiB up, gp2 sustains 250 MB/s and gp3 defaults to 125.
aws ec2 describe-volumes \
--filters Name=volume-type,Values=gp2 \
--query "Volumes[?Size >= \`334\` && Size < \`1000\`].{id:VolumeId,size:Size,az:AvailabilityZone}" \
--output table
# Check IOPS as well: at or above 1000 GiB, confirm real usage before moving.
aws ec2 describe-volumes \
--filters Name=volume-type,Values=gp2 \
--query "Volumes[?Size >= \`1000\`].{id:VolumeId,size:Size,az:AvailabilityZone}" \
--output table
The first list is your easy pile. Move those and bank the 20% with no performance question to answer. The second list moves for $5 a month each, which buys back the throughput gp3 would otherwise drop. The third list is the one you check against CloudWatch first, sizing gp3 IOPS to actual usage.
Run this in every region you operate in. EBS is regional, and a forgotten volume in a region you rarely touch is still billing you every hour.
Migrate online with no downtime
Here is the part that surprises people. You do not detach the volume, snapshot it, or stop the instance. You modify the live volume in place.
aws ec2 modify-volume \
--volume-id vol-0123456789abcdef0 \
--volume-type gp3
The volume enters the optimizing state and stays fully readable and writable the whole time. Your database keeps serving queries. Your app keeps taking traffic. AWS does the conversion in the background.
For a volume of 334 GiB or larger, add the throughput in the same call so it keeps the 250 MB/s it has today.
aws ec2 modify-volume \
--volume-id vol-0123456789abcdef0 \
--volume-type gp3 \
--throughput 250
At or above 1000 GiB, add the IOPS you decided on as well.
aws ec2 modify-volume \
--volume-id vol-0123456789abcdef0 \
--volume-type gp3 \
--iops 12000 \
--throughput 250
Watch the modification move through its states.
aws ec2 describe-volumes-modifications \
--volume-id vol-0123456789abcdef0 \
--query "VolumesModifications[].{state:ModificationState,progress:Progress}" \
--output table
ModificationState walks from modifying to optimizing to completed. The volume is already gp3 and already cheaper once it reaches optimizing. The optimizing phase is AWS settling the performance characteristics, and it does not block your workload.
If something looks wrong, you roll back the same way you rolled forward. Modify the volume back to gp2.
aws ec2 modify-volume \
--volume-id vol-0123456789abcdef0 \
--volume-type gp2
One guardrail to plan around. A single volume can only be modified once every 6 hours. So test on one representative volume, confirm the application is happy, then roll the change out across the easy pile. Do not fire off a modify and a rollback back to back, because the second call will be refused until the window clears.
A quick decision tree
graph TD
A[gp2 volume] --> B{Size >= 334 GiB?}
B -- No --> C[Modify to gp3: same or better baseline, about 20% cheaper]
B -- Yes --> D[Plan on --throughput 250 to hold gp2's sustained 250 MB/s]
D --> E{Size >= 1000 GiB?}
E -- No --> F[Modify to gp3 with default 3000 IOPS and 250 MB/s]
E -- Yes --> G{CloudWatch IOPS usage near the gp2 baseline?}
G -- No --> F
G -- Yes --> H[Provision IOPS to match as well, then recheck the math]
Common questions
Is gp3 cheaper than gp2?
Yes. gp3 lists at about $0.08 per GB-month against gp2's roughly $0.10 in most regions, so the same storage costs about 20% less. gp3 also ships a flat 3000 IOPS and 125 MB/s baseline at no extra charge, so on a volume under 334 GiB you pay less and keep an equal or better performance floor. From 334 GiB up, add --throughput 250 for a flat $5 a month to hold the throughput gp2 was giving you.
Does migrating from gp2 to gp3 cause downtime?
No. A single aws ec2 modify-volume call converts the volume in place while the instance keeps running. The volume stays readable and writable the whole time, so your database keeps serving queries and your app keeps taking traffic. You never detach the volume, snapshot it, or stop the instance.
How long does a gp2 to gp3 migration take?
The volume is already gp3 and already cheaper the moment the modification reaches the optimizing state, which comes quickly. AWS then settles the performance characteristics in the background, and that phase does not block your workload. One limit to plan around: a volume can only be modified once every 6 hours, so test on one volume before you roll the change across the pile.
Will I lose baseline IOPS when I move a large volume to gp3?
Only above 1000 GiB, and only if you take the defaults. gp2 gives 3 IOPS per GB, so a volume larger than 1000 GiB carries a baseline above gp3's default 3000 IOPS. Provision the difference in the same modify-volume call with --iops, and check the volume's real CloudWatch usage first, since most volumes never touch the headroom gp2 handed them.
Will moving to gp3 reduce my throughput?
Yes, on any gp2 volume of 334 GiB or larger, if you take the defaults. AWS documents gp2 as delivering 250 MiB/s from 334 GiB up, while every gp3 volume starts at 125 MB/s regardless of size, so a bare --volume-type gp3 halves it. Pass --throughput 250 in the same modify-volume call to hold the old figure. It costs $0.04 per MB/s-month above the free 125, a flat $5 a month, against $10 of storage saving on a 500 GiB volume.
Why does my gp2 volume suddenly get slow?
It has probably run out of I/O credits. A gp2 volume under 1000 GiB reaches 3000 IOPS by spending from a bucket of 5.4 million credits, and once that bucket empties the volume is throttled to its 3 IOPS per GiB baseline. A 100 GiB volume drops to 300 IOPS when that happens, and it needs about five hours below baseline to refill. Watch the BurstBalance CloudWatch metric, which reports the percentage of credits left.
Does gp3 have burst credits?
No. AWS documents gp3 as sustaining its full provisioned IOPS and throughput indefinitely, with no burst bucket involved. A gp3 volume holds its 3000 IOPS around the clock, so there is no BurstBalance metric for it and no credit cliff to plan around. That is the part of the migration that helps small volumes most, since a gp2 volume of the same size sustains only 3 IOPS per GiB once its credits are gone.
Is gp3 still cheaper if I provision IOPS to match gp2? Yes, at every volume size. Storage saves $0.02 per GiB, matching gp2's 3 IOPS per GiB costs $0.015 per GiB above gp3's free 3000, and matching gp2's 250 MB/s adds a flat $5 a month on volumes of 334 GiB or larger. A 4000 GiB volume goes from $400 a month on gp2 to $370 on gp3 with 12000 IOPS and 250 MB/s matched, so the saving narrows to 7.5% and stays a saving. It is tightest on a 334 GiB volume, $33.40 against $31.72, and AWS states the same conclusion in its migration guidance.
Can I roll back from gp3 to gp2? Yes. Modify the volume back to gp2 with the same command, and it converts in place with no downtime. The one catch is the 6-hour rule. A volume you just changed refuses a second modification until that window clears, so confirm the application is healthy before you decide either way.
How OhChimp approaches this
OhChimp finds the gp2 volumes across your AWS accounts and regions, including the ones in the corners you forgot about. It writes a reviewable gp2-to-gp3 plan with a confidence score, a risk level, and the rollback steps spelled out. The 1000 GiB question is already answered for each volume, so you are reading a recommendation, not redoing the analysis.
You read the plan and click apply. Nothing changes until you do. After the change ships, the saving is checked against your real AWS bill, so you see the dollars that actually left, measured on the invoice.
See how the AWS connection works on the AWS integration page.