Runbook: Reclaim Space on a Data Volume Approaching Full
1 · Prerequisites
Confirm every item is in place before any state change.
- Shell access to the database host with enough privilege to read directory sizes and to remove files that are not PostgreSQL data
- A database connection, if the cluster is still running
- The ability to grow the volume, or an agreed alternative source of space
- Knowledge of whether WAL archiving and replication slots are in use, because both retain WAL regardless of any size setting
- Authority to act quickly, since a volume filling at a measured rate has a deadline
- The two prohibitions in this runbook read and understood before starting, because they will be suggested
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Measure where the space is, by directory.
du -sh /var/lib/postgresql/18/main/* | sort -h | tail -10. This distinguishes a fullpg_walfrom a largebasefrom something that is not PostgreSQL at all. - · **Check
pg_walagainstmax_wal_size.**du -sh <datadir>/pg_walandSHOW max_wal_size;. Apg_walmuch larger thanmax_wal_sizemeans something is retaining WAL, becausemax_wal_sizeis a checkpoint pacing target and not a cap. - · Check the archive backlog.
ls <datadir>/pg_wal/archive_status | grep -c readyandSELECT archived_count, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;. A failing archive retains every unarchived segment. - · Check replication slots.
SELECT slot_name, active, wal_status, safe_wal_size, restart_lsn FROM pg_replication_slots;An inactive slot retains WAL indefinitely, andwal_statusother thanreservedsays so directly. - · Check temporary files.
du -sh <datadir>/base/pgsql_tmpandSELECT datname, temp_files, pg_size_pretty(temp_bytes) FROM pg_stat_database ORDER BY temp_bytes DESC;. A reporting workload can produce hundreds of gigabytes of temporary files in an hour. - · Check the server log directory. Logs frequently share the data volume and are frequently the easiest thing to remove safely.
- · Measure the fill rate. Two
dfreadings a minute apart give you the deadline, which is the number that decides whether you have time to grow the volume or need the ballast file now.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1**Free space from somewhere that is not
pg_wal.** In order of preference: grow the volume; remove non-PostgreSQL files from the same filesystem; delete the ballast file if one exists. - 2Grow the volume if you can. On cloud or LVM storage this is usually minutes, and it is always the correct first answer because it costs nothing else.
- 3Remove things that are not database data. Old server logs, a forgotten dump, a core file, an unpacked tarball.
du -sh /var/lib/postgresql/*andfind <datadir>/.. -maxdepth 2 -size +1Gfind these quickly. - 4Delete the ballast file if the estate keeps one. A pre-allocated file of a few gigabytes exists precisely for this moment. Deleting it buys minutes; it does not fix anything.
- 5Fix the cause of WAL retention, because otherwise the space refills. If the archive is failing, repair the destination or repoint
archive_command, which is reloadable. If an abandoned slot is holding WAL, confirm it is genuinely abandoned and drop it. - 6Watch the backlog drain rather than assuming it will.
watch -n 5 'ls <datadir>/pg_wal/archive_status | grep -c ready'. The incident is over when that count reaches zero andpg_walhas fallen back towardmax_wal_size— not when the cluster starts. - 7If a reporting workload filled the volume with temporary files, cap it per role.
ALTER ROLE reporting SET temp_file_limit = '20GB';This converts "the volume filled" into "one query failed", which needs no operator. - 8**If the cluster has already stopped with a
PANIC, free space first and then start it.** It will replay from the last checkpoint and then work through the archive backlog. Do not change any durability setting to make it start. - 9**Consider moving
pg_walto its own filesystem** as the durable fix. A fullpg_walstill stops the cluster, but it stops only the cluster rather than everything sharing the volume, and its growth becomes a visible metric of its own. - 10**Do not reclaim space with
VACUUM FULLduring business hours.** It takesACCESS EXCLUSIVEon the table for its whole duration and every reader queues behind it. If bloat is genuinely the cause, measure it withpgstattupleand schedule a rewrite properly. - 11Re-measure and record. Where the space went, what was freed, what the fill rate was, and how much headroom now exists expressed in hours rather than percent.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
df -hshows headroom expressed in hours at the measured write rate, not as a percentage. Ninety-five percent of a 20 GB volume is sixteen minutes. - ✓
ls <datadir>/pg_wal/archive_status | grep -c readyreturns zero, andpg_stat_archiver.failed_counthas stopped increasing. - ✓
du -sh <datadir>/pg_walhas fallen back to roughlymax_wal_sizeplus normal churn. - ✓
SELECT slot_name, active, wal_status FROM pg_replication_slots;shows no slot with awal_statusother thanreserved. - ✓A forced archive round trip lands at the destination:
SELECT pg_switch_wal();then confirm the segment appears. A quiet archiver looks the same whether it is healthy or doing nothing. - ✓The cluster accepts connections and
pg_is_in_recovery()returns false. - ✓No durability setting was changed.
SELECT name, setting FROM pg_settings WHERE name IN ('fsync','full_page_writes','synchronous_commit');matches what it was before. - ✓The alerting gap that let the volume reach this state has been identified and a follow-up action recorded.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Freeing space is not reversible in a meaningful sense, and does not need to be. The rollback section here covers the changes that accompany it.
- ↶If
archive_commandwas repointed, revert it once the original destination is healthy, and confirm the segments archived to the temporary location are copied to the permanent one. WAL that exists only in a temporary location is a gap in the recovery window. - ↶If a replication slot was dropped, the standby that used it may no longer be able to catch up. Confirm it reconnected; if the WAL it needed is gone, rebuild it from a base backup.
- ↶If
temp_file_limitwas set aggressively during the incident, review it afterwards against what the reporting workload actually needs rather than leaving an emergency value in place. - ↶If the ballast file was deleted, recreate it once the volume has headroom. A ballast file that was used and never restored is not available for the next incident.
- ↶If files were removed and it later turns out something needed them, they are gone. This is why the procedure removes non-PostgreSQL files by name rather than by pattern.
6 · Escalation
When the runbook isn't enough, contact:
- · Somebody proposes deleting files from
pg_wal: escalate and stop them. Those segments hold transactions already acknowledged to clients; deleting them destroys the ability to recover and the cluster will refuse to start once it notices the gap. - · Somebody proposes
fsync = offto make the cluster start: escalate and stop them. It removes the guarantee that anything committed afterwards survives a crash, on a machine that has just fallen over, and converts one outage into silent corruption. - · The archive destination cannot be restored quickly: escalate to whoever owns it. Repointing
archive_commandat temporary storage is a legitimate emergency action and it creates a follow-up obligation to consolidate the archive. - · A replication slot is holding WAL and its consumer cannot be identified: escalate rather than dropping it. Dropping a slot whose standby is merely slow forces a rebuild of that standby.
- · The volume cannot be grown and there is nothing safe to delete: escalate to the platform owner immediately with the measured fill rate and the resulting deadline. This is a capacity emergency, not a database one.
- · The cluster has already PANICked and space cannot be freed: escalate. The cluster is down and every minute of a wrong decision — a manual WAL deletion, a durability setting — makes the recovery worse.
Two things will be suggested during this incident, probably within minutes, and both of them convert a recoverable outage into permanent data loss.
The two prohibitions
Both suggestions arrive because they are the obvious wrong answers. Put them in the runbook, in writing, so they are pre-empted rather than argued about at 02:14.
Where the space actually goes
du -sh /var/lib/postgresql/18/main/* | sort -h | tail -10
| Directory | What a large size means |
|---|---|
pg_wal | Something is retaining WAL — the archive, a slot, or a very high write rate |
base | Real data, or bloat; measure with pgstattuple before assuming |
base/pgsql_tmp | Temporary files from sorts and hashes that exceeded work_mem |
log | Server logs, frequently the easiest safe thing to remove |
| anything else | Not PostgreSQL; usually the fastest space to reclaim |
max_wal_size is not a cap
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
| Growing the volume | Yes | Nothing; always the right first answer |
| Removing non-PostgreSQL files | No | Whatever they were |
| Deleting the ballast file | Yes, recreate it | Nothing, and it buys minutes |
Repointing archive_command | Yes | A split archive that must be consolidated afterwards |
| Dropping a replication slot | No | A standby that may need a full rebuild |
Deleting files from pg_wal | No | Acknowledged transactions; the cluster will not start |
fsync = off | No | Every future commit’s durability guarantee |
VACUUM FULL to reclaim space | Yes | An ACCESS EXCLUSIVE lock and a queue behind it |
The alert that should have fired
The incident is over when the backlog is zero
Not when the cluster starts. Not when df looks better.
watch -n 5 'ls /var/lib/postgresql/18/main/pg_wal/archive_status | grep -c ready'
When that reaches zero and pg_wal has fallen back toward
max_wal_size, the cause is fixed. Until then you have bought time.