Skip to content

RDS storage full: emergency recovery and autoscaling storage

An RDS instance in STORAGE_FULL status stops accepting writes. Here is the emergency recovery — grow the volume, reclaim space — and how storage autoscaling prevents the repeat.

When an RDS instance shows STORAGE_FULL status, the engine can no longer write — inserts fail, and on Postgres even reads can stall because the engine cannot write WAL or temp files. The fastest recovery is to increase allocated storage from the console or CLI, which works even while the instance is in STORAGE_FULL. Then find what ate the disk — binary logs, log tables, temp files, or a stalled replication slot — because growing the volume without finding the eater just schedules the next incident.

What’s happening

RDS instances on MySQL, Postgres, MariaDB, and SQL Server sit on an EBS volume of fixed allocated size (Aurora is different — its cluster storage grows automatically, so this article is about non-Aurora RDS). When FreeStorageSpace hits zero, RDS puts the instance in STORAGE_FULL and write operations fail. You cannot shrink the volume later; storage only grows.

Space rarely disappears because the table data grew smoothly. The usual culprits are the side channels: MySQL binary logs accumulating because binlog retention hours was raised or a read replica stopped consuming them; the MySQL general or slow query log written to tables (log_output = TABLE) and never rotated; Postgres WAL piling up because a replication slot is stalled — the slot pins WAL forever until the consumer catches up or the slot is dropped; enormous temp files from a runaway query doing an on-disk sort; and plain old table bloat on Postgres from a workload with heavy updates and insufficient vacuum.

One operational trap worth knowing before you touch anything: after any storage modification, RDS blocks further storage changes for six hours or until storage optimization completes, whichever is longer. Grow generously the first time — you do not get a second attempt for hours.

What to check first

  • FreeStorageSpace slope in RDS > Databases > your-instance > Monitoring — a slow decline says data growth; a cliff says logs, temp files, or a stalled slot.
  • Instance status — STORAGE_FULL confirms the diagnosis; if the status is available but writes fail, you have a different problem.
  • Binary log retention on MySQL: CALL mysql.rds_show_configuration; shows binlog retention hours.
  • Replication slots on Postgres — one stalled slot can pin hundreds of GB of WAL.
  • Recent events in RDS > Events — RDS emits low-storage events before it hits the wall.

How to verify

Check the metric and the allocation:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name FreeStorageSpace \
  --dimensions Name=DBInstanceIdentifier,Value=prod-db \
  --start-time 2026-07-02T00:00:00Z --end-time 2026-07-04T12:00:00Z \
  --period 3600 --statistics Minimum

aws rds describe-db-instances --db-instance-identifier prod-db \
  --query 'DBInstances[0].[AllocatedStorage,MaxAllocatedStorage,DBInstanceStatus]'

If MaxAllocatedStorage is null, storage autoscaling is off. Inside the engine, find the eaters:

-- MySQL: binary log disk usage
SHOW BINARY LOGS;

-- MySQL: biggest schemas
SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024/1024,2) AS gb
FROM information_schema.tables GROUP BY table_schema ORDER BY gb DESC;

-- Postgres: stalled replication slots pinning WAL
SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;

-- Postgres: biggest relations
SELECT relname, pg_size_pretty(pg_total_relation_size(oid))
FROM pg_class ORDER BY pg_total_relation_size(oid) DESC LIMIT 10;

The runbook

  1. Grow the volume now. aws rds modify-db-instance --db-instance-identifier prod-db --allocated-storage 300 --apply-immediately. This works during STORAGE_FULL, needs no reboot, and the instance stays online while the volume scales. AWS requires the increase to be at least 10% over current; given the six-hour lockout, add 25-50%, not the minimum.
  2. Purge MySQL binary logs if they are the eater. Lower retention with CALL mysql.rds_set_configuration('binlog retention hours', 24); — RDS purges logs past the window automatically, but only if no replica still needs them. Confirm replica health first.
  3. Drop the stalled Postgres slot. If pg_replication_slots shows an inactive slot retaining tens of GB, and its consumer (a DMS task, a CDC pipeline, a dead replica) is gone for good, SELECT pg_drop_replication_slot('slot_name');. The retained WAL frees within minutes. If the consumer is coming back, fix the consumer instead — dropping the slot forces a full resync.
  4. Rotate log tables. If log_output is TABLE on MySQL, run CALL mysql.rds_rotate_slow_log; and CALL mysql.rds_rotate_general_log;, then switch log_output to FILE in the parameter group so file-based rotation handles it.
  5. Clear the temp-file source. Kill the query doing a giant on-disk sort (visible in pg_stat_activity / SHOW PROCESSLIST); its temp space frees on termination.
  6. Enable storage autoscaling before you close the incident. Set --max-allocated-storage to a ceiling you accept paying for. RDS then grows storage automatically when free space drops below 10% for five minutes — subject to the same six-hour cooldown between increments, which is why autoscaling is a safety net, not a substitute for capacity planning.

When this is the wrong move

If storage filled because a replication slot is stalled or logs are misconfigured, growing the volume first is still correct — you need the engine writable to fix anything — but stopping there is wrong: the eater will consume the new space at the same rate. Conversely, if the growth is genuine business data on a steep slope, autoscaling to an ever-higher ceiling is just deferring an archiving or partitioning conversation. Storage-per-GB billing on gp3 is modest, but IOPS and snapshot costs scale with it. And if you are on Aurora, none of this applies — see Aurora vs RDS for how the storage models differ.

Common mistakes

  • Growing storage by the minimum 10%, hitting the wall again in an hour, and being locked out of further modifications for six hours.
  • Dropping a Postgres replication slot that a live CDC pipeline depended on, forcing a full-table resync during business hours.
  • Deleting rows to free space and expecting FreeStorageSpace to move — on Postgres, deletes do not return space to the OS without a vacuum full or table rewrite; on InnoDB, only file_per_table tablespaces shrink, and only on optimize.
  • Confusing local storage on the instance class with EBS allocated storage — temp-heavy SQL Server and Postgres workloads exhaust them differently.

Prevention

  • Alarm on FreeStorageSpace at 20% and 10% of allocated storage — absolute-value alarms go stale as the volume grows.
  • Enable storage autoscaling with a deliberate MaxAllocatedStorage ceiling on every production instance.
  • Review binlog retention and replication slots monthly; both fail silently.
  • Keep log_output = FILE on MySQL and let RDS rotation manage engine logs.
  • Watch the slope, not the level: a FreeStorageSpace trend line that hits zero inside your snapshot retention window deserves a ticket now. Snapshot strategy matters here too — see the PITR and snapshot runbook.

A full disk is the most preventable RDS outage there is: the metric is public, the slope is visible weeks ahead, and the fix is a CLI call. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.