Storage for SQL Server: gp3 vs io2, tempdb

“The database is slow” is very often “the storage is slow”, and on AWS that usually means a limit someone didn’t know about. SQL Server storage on AWS has three: the volume (gp3 or io2), the instance class’s own EBS bandwidth, and how SQL Server lays out data, log and tempdb files.

You’ll check saat-sql’s storage and instance limits, push real I/O through it, read per-file latency inside SQL Server and the matching CloudWatch metrics, find where tempdb lives, and turn on storage autoscaling. Then you’ll hit two walls: io2’s minimum size and Express’s 10 GB limit.

TimeYou needBy the end you can
50 minutesLesson 42 done (saat-sql, Start-SqlLab.ps1, Stop-SqlLab.ps1), your laptop signed in as saat-admin. Cost: about USD 0.20 for the session; nothing is upgraded.Read RDS storage settings and instance EBS limits, generate and measure SQL Server I/O with sys.dm_io_virtual_file_stats and CloudWatch, understand gp3, io2 and instance store tempdb, and configure storage autoscaling.

What it is, in one line

SQL Server storage on AWS is the EBS volume type and size behind your database, limited by both the volume’s IOPS and throughput and the instance class’s EBS bandwidth.

In Simple Words

A water tank, a pipe and a tap

A big water tank (the volume) is useless if the pipe from it (the instance’s EBS bandwidth) is thin. Customers turning on taps (queries) only get what the thinnest part allows.

Small instance classes have a thin pipe that can widen for short bursts, like a pipe that stretches for a few minutes and then shrinks back. When the stretch is used up, everything slows down.

tempdb is the kitchen sink everyone uses at once. Putting it on its own fast local tap (instance store) keeps it from slowing down the main supply.

Engineer's View

How storage really behaves for RDS SQL Server

gp3 includes a baseline of 3,000 IOPS and 125 MiB/s and, since June 2026 on RDS for SQL Server, scales to 64 TiB, 80,000 IOPS and 2,000 MiB/s. io2 Block Express provisions IOPS up to 256,000 with sub-millisecond latency and 99.999% durability; on RDS SQL Server it needs at least 100 GiB. Storage can grow but never shrink, and after a storage change you must wait (usually six hours) before the next one.

Every instance class has its own EBS bandwidth and IOPS limits. Burstable classes such as db.t3 have a low baseline and burst credits; CloudWatch’s EBSIOBalance% and EBSByteBalance% show how much burst is left. When they hit zero, I/O drops to the baseline whatever the volume could do.

On RDS, data, log and tempdb share the instance’s storage, except on classes with local NVMe instance store (for example db.m5d, db.r5d, db.x2iedn), where RDS places tempdb on the fast local disk. Inside SQL Server, sys.dm_io_virtual_file_stats gives per-file read and write latency; as a rough guide, data under 10 ms and log under 2 ms is healthy. Storage autoscaling grows the volume when free space runs low, up to MaxAllocatedStorage.

How it works, step by step

Here is what limits a write from SQL Server.

1SQL Server issues the I/O

Log writes must finish before a transaction commits, so log latency is felt directly by users. Data writes happen in the background at checkpoints.

The three limits on SQL Server I/O in RDS: instance class, volume, latency

2The instance class carries it

db.t3.micro has a baseline of about 87 Mbps (roughly 11 MB/s) and 500 IOPS, bursting much higher for a while. That is far below what even a basic gp3 volume can do.

3The volume serves it

gp3 gives 3,000 IOPS and 125 MiB/s without extra charges. Raising volume IOPS on a burstable instance often changes nothing.

4Pick the right storage

Here’s how the options compare:

gp3 vs io2 for SQL Server on RDS, instance store tempdb and storage autoscaling

Before paying for io2, check the instance class limits and the burst balances. The cheapest fix is often a non-burstable class with higher EBS bandwidth, or a d class for tempdb-heavy work.

✅ Checkpoint: You can name the three I/O limits, explain burst balance, say where tempdb lives on RDS, and when io2 is worth it.

Hands-on lab: measure SQL Server storage on AWS

Sign in and start the SQL lab (saat-sql starts if it’s stopped; allow 5 to 10 minutes):

cd C:\aws-lab
aws login --profile saat-admin
$env:AWS_PROFILE = 'saat-admin'
.\Start-SqlLab.ps1

You should see something like this:

Tunnel ready on 127.0.0.1,11433 for saatadmin (password is in SQLCMDPASSWORD for this window)

1Read the storage and instance limits

Paste this whole block:

aws rds describe-db-instances --db-instance-identifier saat-sql --query "DBInstances[0].[DBInstanceClass,StorageType,AllocatedStorage,Iops,StorageThroughput,MaxAllocatedStorage]" --output text
aws ec2 describe-instance-types --instance-types t3.micro --query "InstanceTypes[0].EbsInfo.EbsOptimizedInfo.[BaselineBandwidthInMbps,MaximumBandwidthInMbps,BaselineIops,MaximumIops]" --output text

You should see something like this:

db.t3.micro     gp3     20      3000    125     None
87      2085    500     11800

The volume offers 3,000 IOPS and 125 MiB/s. The instance underneath (db.t3.micro uses the same hardware as t3.micro) has a baseline of only 500 IOPS and 87 Mbps, bursting to 11,800 IOPS and 2,085 Mbps while credits last. MaxAllocatedStorage None means autoscaling is off.

2Push real I/O through SQL Server

Paste this whole block. It writes about 200 MB of rows (one to two minutes on this small instance):

@'
SET NOCOUNT ON;
IF OBJECT_ID('dbo.Big') IS NULL CREATE TABLE dbo.Big (Id int IDENTITY PRIMARY KEY, Pad char(4000) NOT NULL DEFAULT 'x');
INSERT dbo.Big (Pad) SELECT TOP (50000) 'x' FROM sys.all_objects a CROSS JOIN sys.all_objects b;
CHECKPOINT;
SELECT COUNT(*) AS big_rows, CAST(SUM(DATALENGTH(Pad)) / 1048576.0 AS decimal(10,1)) AS approx_mb FROM dbo.Big;
'@ | Set-Content io-work.sql
sqlcmd -S "127.0.0.1,11433" -U saatadmin -C -d SaatShop -i io-work.sql -W

You should see something like this:

big_rows approx_mb
50000    190.7

3Measure latency inside SQL Server and in CloudWatch

Paste this whole block. Wait a minute or two first so CloudWatch has the datapoints:

@'
SET NOCOUNT ON;
SELECT DB_NAME(vfs.database_id) AS db, mf.type_desc, vfs.num_of_reads, vfs.num_of_writes,
  CAST(vfs.io_stall_read_ms * 1.0 / NULLIF(vfs.num_of_reads, 0) AS decimal(10,1)) AS avg_read_ms,
  CAST(vfs.io_stall_write_ms * 1.0 / NULLIF(vfs.num_of_writes, 0) AS decimal(10,1)) AS avg_write_ms
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf ON mf.database_id = vfs.database_id AND mf.file_id = vfs.file_id
WHERE vfs.database_id IN (DB_ID('SaatShop'), 2) AND mf.type_desc IN ('ROWS', 'LOG')
ORDER BY db, mf.type_desc;
'@ | Set-Content io-stats.sql
sqlcmd -S "127.0.0.1,11433" -U saatadmin -C -i io-stats.sql -W
function Get-RdsMetric([string]$Name, [string]$Stat = 'Average', [int]$Minutes = 15) {
  $end = (Get-Date).ToUniversalTime(); $start = $end.AddMinutes(-$Minutes)
  $vals = aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name $Name --dimensions Name=DBInstanceIdentifier,Value=saat-sql --start-time $start.ToString('o') --end-time $end.ToString('o') --period 60 --statistics $Stat --query "Datapoints[].$Stat" --output text
  if ($Stat -eq 'Minimum') { ($vals -split '\s+' | ForEach-Object { [double]$_ } | Measure-Object -Minimum).Minimum } else { ($vals -split '\s+' | ForEach-Object { [double]$_ } | Measure-Object -Maximum).Maximum }
}
"WriteIOPS max:       $(Get-RdsMetric WriteIOPS Maximum)"
"WriteLatency max s:  $(Get-RdsMetric WriteLatency Maximum)"
"EBSIOBalance% min:   $(Get-RdsMetric EBSIOBalance% Minimum)"

You should see something like this:

db       type_desc num_of_reads num_of_writes avg_read_ms avg_write_ms
SaatShop LOG       12           51840         0.8         0.6
SaatShop ROWS      3311         26402         2.1         1.4
tempdb   LOG       9            61            0.7         0.9
tempdb   ROWS      40           88            0.9         1.2
WriteIOPS max:       2950.4
WriteLatency max s:  0.0021
EBSIOBalance% min:   71

Latency is fine because the instance was bursting: a few minutes of load used almost a third of its I/O burst balance. A real daytime workload on db.t3.micro would drain it, and then the same writes would queue. That’s the pattern behind many “it was fast this morning” tickets.

SQL Server storage on AWS lab output: file latency, WriteIOPS and EBSIOBalance
File latency, WriteIOPS and burst balance. Something like this.

4Find tempdb and turn on autoscaling

Paste this whole block:

sqlcmd -S "127.0.0.1,11433" -U saatadmin -C -d tempdb -W -Q "SET NOCOUNT ON; SELECT name, physical_name, size * 8 / 1024 AS size_mb FROM sys.database_files"
aws rds modify-db-instance --db-instance-identifier saat-sql --max-allocated-storage 100 --apply-immediately --query "DBInstance.PendingModifiedValues" --output json | Out-Null
Start-Sleep 30
aws rds describe-db-instances --db-instance-identifier saat-sql --query "DBInstances[0].MaxAllocatedStorage" --output text

You should see something like this:

name     physical_name                  size_mb
tempdev  D:\rdsdbdata\DATA\tempdb.mdf     8
templog  D:\rdsdbdata\DATA\templog.ldf    8
temp2    D:\rdsdbdata\DATA\tempdb2.ndf    8
100

tempdb shares the D: data volume here. On a class with instance store, the same query shows tempdb on a separate fast local drive. Autoscaling now grows saat-sql up to 100 GiB if it runs low on space, instead of the database stopping with “disk full”.

RDS console storage settings for saat-sql with gp3 and storage autoscaling up to 100 GiB
saat-sql’s storage settings in the RDS console: gp3, with autoscaling up to 100 GiB.

✅ Checkpoint: You read the gp3 volume and the much lower instance baseline, generated about 200 MB of writes, measured per-file latency and EBSIOBalance%, found tempdb on D:, and set MaxAllocatedStorage to 100.

Break It, Then Fix It

Two storage walls that are much cheaper to know about than to hit.

1Break it: switch a small volume to io2

Try to move the 20 GiB volume to io2 with 1,000 IOPS:

aws rds modify-db-instance --db-instance-identifier saat-sql --storage-type io2 --iops 1000 --apply-immediately

You’ll get this error:

An error occurred (InvalidParameterCombination) when calling the ModifyDBInstance operation: Invalid storage size for engine name sqlserver-ex and storage type io2: 20
Storage problems: io2 minimum size on RDS and the Express 10 GB database limit
Both storage walls. Something like this.

io2 on RDS for SQL Server starts at 100 GiB. And there’s a trap behind the fix: if you grow the volume to 100 GiB to make io2 possible, you can never shrink it back, so you pay for 100 GiB from then on. Fix: check limits before changing storage, and test io2 on a restored copy (Lesson 52), not on the original:

$v22 = aws rds describe-db-instances --db-instance-identifier saat-sql --query "DBInstances[0].EngineVersion" --output text
aws rds describe-orderable-db-instance-options --engine sqlserver-ex --engine-version $v22 --db-instance-class db.t3.micro --query "OrderableDBInstanceOptions[?StorageType=='io2'] | [0].[MinStorageSize,MinIopsPerDbInstance]" --output text

You should see something like this:

100     1000

2Break it: grow a database past the Express limit

Try to pre-size SaatShop’s data file to 11 GB:

sqlcmd -S "127.0.0.1,11433" -U saatadmin -C -Q "ALTER DATABASE SaatShop MODIFY FILE (NAME = SaatShop, SIZE = 11GB)"

You’ll get an error like this:

Msg 1827, Level 16, State 1, Server EC2AMAZ-ABC123, Line 1
CREATE DATABASE or ALTER DATABASE failed because the resulting cumulative database size would exceed your licensed limit of 10240 MB per database.

Express caps each database at 10 GB of data, however big the volume is. In production this appears at the worst time: inserts fail with error 1105 or 1827 when the database tries to grow. Fix: Express only for small, non-critical databases; move to Standard (Lesson 53 covers changing editions through snapshot restore).

Now clean up the test data and stop the lab:

sqlcmd -S "127.0.0.1,11433" -U saatadmin -C -d SaatShop -Q "DROP TABLE dbo.Big; DBCC SHRINKFILE (SaatShop_log, 64);"
Remove-Item io-work.sql, io-stats.sql
.\Stop-SqlLab.ps1

You should see something like this:

DBCC execution completed. If DBCC printed error messages, contact your system administrator.
stopping
Close the tunnel window if it is still open.

✅ Checkpoint: You saw io2 refused below 100 GiB and Express refuse an 11 GB file. The test table is gone, autoscaling is on, and saat-sql is stopping.

This happened at work

The faster disk that didn’t help

An accounting firm’s month-end close ran on RDS SQL Server, db.t3.xlarge with 500 GiB gp2. Every month-end, reports slowed to a crawl after about 30 minutes. The team bought io1 with 10,000 IOPS, doubling the storage bill.

Month-end was still slow. CloudWatch showed why: EBSIOBalance% and EBSByteBalance% dropped to zero 30 minutes into the run, and from then on the instance class’s baseline bandwidth capped everything, far below what the io1 volume could do. Read latency jumped from 2 ms to over 40 ms.

They moved to db.r6i.xlarge (non-burstable, with much higher EBS bandwidth) on gp3 with 6,000 IOPS, cheaper than the io1 setup, and month-end finished in under an hour. They now alarm on burst balance below 30 percent for any burstable database.

Common mistakes and troubleshooting

MistakeWhat happensWhat to do
Buying volume IOPS for a burstable instanceNo improvement once burst credits run outCheck EBSIOBalance% and instance limits first
Growing storage to try io2Can never shrink back; permanent costTest on a restored copy
Express for growing databasesError 1827 at 10 GBStandard or above for anything that grows
No storage autoscalingDatabase stops on a full diskSet MaxAllocatedStorage with an alarm on FreeStorageSpace
Ignoring log latencySlow commits for every userWatch avg_write_ms on log files; keep it low
tempdb-heavy work on non-d classestempdb contention on shared storageConsider d classes with instance store tempdb

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. What’s the difference between gp3 and io2 storage, and when would you pick each for SQL Server?

Why they ask this: They want to know you understand the main storage options and their cost. Storage choice has a big effect on database speed and on the bill.

How you might answer: gp3 gives a baseline of 3,000 IOPS and 125 MiB/s, and you can scale IOPS and throughput separately, at a low cost. io2 lets you provision IOPS for consistent, very high performance and higher durability, but it costs more.

For most SQL Server workloads I’d start with gp3, and move to io2 only when the latency needs really demand it.

2. A developer says the database feels slow and asks if the disks are the problem. How would you check SQL Server file latency?

Why they ask this: They are checking basic DBA skills. They want to see that you measure before blaming the storage.

How you might answer: I’d query sys.dm_io_virtual_file_stats and divide io_stall_read_ms and io_stall_write_ms by the number of reads and writes for each file. That gives me the average latency per file, so I can see if data or log files are slow.

3. You gave an RDS instance far more storage than it needs. Can you shrink it back down?

Why they ask this: They want to know you understand RDS storage limits. This affects how carefully you size storage at the start.

How you might answer: No, RDS storage can only grow, not shrink. To get smaller storage I’d have to migrate the database to a new instance, so I size it carefully and let autoscaling grow it when needed.

2–5 years experience

4. Database I/O is slow, but the volume still has spare IOPS. What would you check next?

Why they ask this: They want to see that you look beyond the volume settings. Many I/O problems come from the instance class or the workload, not the disk.

How you might answer: I’d check the instance class EBS limits and its burst balances, EBSIOBalance% and EBSByteBalance%. I’d also compare throughput with IOPS limits and look at queue depth.

Then I’d check for CPU or memory pressure causing extra reads, and whether the workload is limited by the log writes.

5. Where does tempdb live on RDS for SQL Server?

Why they ask this: They are checking that you know how RDS lays out SQL Server internally. tempdb placement affects performance for heavy workloads.

How you might answer: By default tempdb sits on the data volume, the D: drive. On instance classes that have instance store, RDS puts tempdb on the local NVMe disk, which is faster.

6. How would you explain storage autoscaling on RDS to a new team member?

Why they ask this: They want to see that you know how to prevent outages from full disks. A full disk takes a database down quickly.

How you might answer: I’d say it watches free space, and when free space gets low, RDS increases the allocated storage on its own, up to the MaxAllocatedStorage limit we set. That way the database doesn’t go down just because the disk filled up overnight.

Senior level

7. You’re asked to design storage for a 4 TB OLTP SQL Server on RDS that peaks at 20,000 IOPS. How would you approach it?

Why they ask this: They want to see capacity planning across instance, storage and monitoring. They are checking that you test your design instead of guessing.

How you might answer: I’d pick a non-burstable instance class whose EBS bandwidth covers the peak throughput, because the instance can be the limit even if the disk isn’t. For storage I’d use gp3 with provisioned IOPS and throughput, or io2 if the latency SLAs demand it.

I’d turn on autoscaling with headroom, set alarms on latency, queue depth and free space, and load test on a restored copy before go-live.

8. Someone wants to move a database to io2 because it should be faster. How would you prove it’s worth the extra cost?

Why they ask this: They want to see evidence-based decisions. io2 costs more, and they want to know you test before spending.

How you might answer: I’d restore a snapshot to a test instance and replay a captured workload, first on gp3 with tuned IOPS, then on io2. I’d compare latency percentiles and throughput between the two.

I’d also check whether the instance class is the real limit, because then io2 won’t help at all.

Quick quiz

Q1. What baseline does gp3 include?

  1. 100 IOPS
  2. 3,000 IOPS and 125 MiB/s
  3. 16,000 IOPS
  4. None
Show answer

B — gp3 includes 3,000 IOPS and 125 MiB/s.

Q2. Why might more volume IOPS not help a db.t3 instance?

  1. gp3 is broken
  2. The instance class EBS limit is lower
  3. SQL Server ignores IOPS
  4. Express blocks it
Show answer

B — Burstable classes have low baselines.

Q3. What is the minimum io2 size on RDS for SQL Server?

  1. 20 GiB
  2. 50 GiB
  3. 100 GiB
  4. 1 TiB
Show answer

C — And storage can never shrink.

Q4. What limit did Express hit at 11 GB?

  1. Volume size
  2. 10 GB per database licence limit
  3. IOPS
  4. tempdb
Show answer

B — Error 1827.

Q5. Where does RDS put tempdb on d instance classes?

  1. S3
  2. Local NVMe instance store
  3. A second gp3 volume
  4. Memory only
Show answer

B — Fast local disk for tempdb.

Quick recap

  • I/O is limited by the instance class, the volume and file layout; the lowest wins.
  • Watch EBSIOBalance% and EBSByteBalance% on burstable classes.
  • gp3 covers most workloads; io2 starts at 100 GiB and storage never shrinks.
  • Measure latency with sys.dm_io_virtual_file_stats and CloudWatch.
  • Turn on storage autoscaling; Express stops at 10 GB per database.

Next lesson

Lesson 44: Secure RDS SQL Server: KMS, TLS, security groups → You’ll force encrypted connections, verify the server certificate properly, and lock down network access to saat-sql.

Further reading: SQL Server storage on AWS

Want to go deeper into SQL Server storage on AWS? Read Amazon RDS DB instance storage and Instance store support for the tempdb database on Amazon RDS for SQL Server. You can also see the full plan for this course on the AWS Cloud Administration course page.

Scroll to Top