When the Postgres disk fills, free space outside Postgres first, then deal with the server.
DELETE and VACUUM free nothing, CHECKPOINT crashes a full server, and DROP TABLE frees space at once, as does TRUNCATE unless the table has an index.
In my lab every acknowledged commit survived recovery, unless I deleted WAL by hand.
I filled a 512 MB ext4 loop file mounted as the Postgres volume, on PostgreSQL 18.6 with default settings and Docker 29.9.3, in a Docker-in-Docker container under WSL2. Every crash here was a process crash in that lab, not a machine or power failure, so a real disk's write cache and power loss are untested. I also didn't test other filesystems, a real VPS disk, or a provider's volume resize.
What Do You Do First When Postgres Is Down on a Full Disk?
Work in this order, because no statement inside Postgres is a safe first move.
/mnt/pg is the volume's path on the host.
This prints yours, on an exited container too:
docker inspect --format '{{range .Mounts}}{{.Source}} {{.Destination}}{{"\n"}}{{end}}' postgres
The first column is the path.
If the second is /var/lib/postgresql/data (the usual mount before 18), pg_wal and base sit directly under that path, not under 18/docker.
postgres is the container and shop the database, so use your names.
I ran everything as root, so run these from a root shell, because a non-root df on a named volume's path failed.
Step 1: Look Without Writing
These only read, and they answered at 100 percent on my volume, which had its own filesystem.
On a shared root disk docker exec can fail as well, as my Docker guide measured, so rely on the other lines until there's room.
df -h /mnt/pg
du -sh /mnt/pg/18/docker/pg_wal /mnt/pg/18/docker/base
docker inspect --format '{{.State.Status}} exit={{.State.ExitCode}} restarts={{.RestartCount}}' postgres
docker logs --tail 12 postgres
docker exec postgres pg_isready -U postgres
A green pg_isready proves nothing here, because it printed accepting connections while writes and new connections failed.
Don't delete anything in pg_wal, even when it holds half the disk.
That folder is the write-ahead log (WAL), and deleting its newest files started the server cleanly with 53 percent of acknowledged batches gone.
Step 2: Free Space Outside Postgres
Stop whatever is writing, then look for container logs, old images, or a stray dump.
My Docker no space guide covers those cleanups and what each one also removes.
If Postgres has exited, skip docker container prune and every system prune: they removed the stopped container, 6 of 6, and --volumes can take an anonymous volume's data.
My lab's room came from a ballast, a spare 64 MB file kept for this moment:
rm /mnt/pg/ballast
docker start postgres
docker exec postgres pg_isready -U postgres
df -h /mnt/pg
Free until df shows more than 16 MB under Avail, because recovery needs room for one new WAL segment, and on ext4 the root reserve can absorb what you delete first.
With 8 or 12 MB free, recovery exited with code 1, 3 of 3, and with 20 or 32 MB it started, 3 of 3.
If nothing outside Postgres can go, the options are an expendable table, covered below, if the server is up, or the filesystem's reserved blocks. This reads that reserve and changes nothing:
df --output=source,fstype /mnt/pg | tail -1
tune2fs -l "$(df --output=source /mnt/pg | tail -1)" | grep -E 'Reserved block count|Block size'
Handing part of it back is a stopgap, for an ext disk only, with the writer stopped.
On a shared root disk, that reserve is what root-run daemons are living on, and the change survives a reboot.
Never take it to zero, because in my Docker lab root writes and docker exec then failed.
tune2fs -m 1 "$(df --output=source /mnt/pg | tail -1)"
That lowers the reserve to 1 percent. This is its undo, to run only after the disk is well under 95 percent:
tune2fs -m 5 "$(df --output=source /mnt/pg | tail -1)"
df -h /mnt/pg
Run early, Avail went back to 0 and the next CHECKPOINT crashed the server, 3 of 3.
-m 5 restores the mkfs.ext4 default, so if your disk started with another value, use that instead.
Step 3: Find What Holds the Space
Run these only after step 2:
docker exec postgres psql -U postgres -d shop -c "SELECT slot_name, active, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots;"
docker exec postgres psql -U postgres -d shop -c "SELECT archived_count, failed_count, last_failed_wal FROM pg_stat_archiver;"
docker exec postgres psql -U postgres -d shop -c "SELECT pg_size_pretty(pg_database_size('shop'));"
A small database on a full disk points at WAL retention, because an inactive slot or a failing archive_command makes Postgres keep old WAL.
A dead slot shows active as f and a large retained.
If both are clean and the database itself is large, the fix is more disk.
Step 4: Fix the Cause
Dropping a slot is destructive for a replica that is merely behind, because that replica must come back through a new base backup.
Confirm that nothing uses the slot, and replace dead_replica with its name.
docker exec postgres psql -U postgres -d shop -c "SELECT pg_drop_replication_slot('dead_replica');"
docker exec postgres psql -U postgres -d shop -c 'CHECKPOINT;'
docker exec postgres psql -U postgres -d shop -c 'SELECT pg_switch_wal();'
df -h /mnt/pg
CHECKPOINT at zero free crashed the server 3 of 3, and pg_switch_wal() starts a new segment, so both need the room from step 2.
Fixing the cause frees little at first, because pg_wal only shrinks as WAL moves on.
At default settings the disk stayed at 90 percent after the fix, a CHECKPOINT, and 5 rounds of pg_switch_wal() plus CHECKPOINT, then read 76 percent after 10 rounds and 47 after 20, in 3 of 3 runs.
Normal writes should move WAL the same way, but I only measured those rounds, and only after dropping a slot.
With archive_command still failing and max_wal_size at 128 MB, they crashed the server 3 of 3, and I didn't validate a repaired one.
Step 5: Check the Data
This needs room:
docker exec postgres pg_amcheck -U postgres --all --install-missing --heapallindexed
Straight after the 64 MB ballast, on an indexed 139 MB table nobody had read, it crashed the server 3 of 3 and took the disk back to 100 percent. In the full order above, with 50 MB free and recycled WAL on hand, it passed 3 of 3.
So free more than the size of the rows nobody has read yet.
--install-missing adds the amcheck extension to every database it checks.
What Happens When Postgres Runs Out of Disk?
Postgres writes WAL in 16 MB segments and recycles old ones after a checkpoint.
A bulk load with default settings had no recycled segment, so it needed a new file and crashed with df at 99 percent and 5 MB free.
With recycled WAL on hand, the data file ran out first, and the statement got an error while the server stayed up:
PANIC: could not write to file "pg_wal/xlogtemp.74": No space left on device
ERROR: could not extend file "base/16384/16386": No space left on device
The server staying up doesn't last, because at zero free the next checkpoint crashed it, about 292 seconds later here.
| Stage | What the log says | What the container does |
|---|---|---|
| Data file needs a block first | ERROR, could not extend file, SQLSTATE 53100 | Up. Writes fail, pg_isready and a default AddNpgSql readiness probe stay green |
| Next checkpoint at zero free | PANIC on pg_logical/replorigin_checkpoint.tmp | Crashes and restarts. docker stop also panics, 7 of 7 |
| After that restart, little free | FATAL: could not write init file | Up. pg_isready says accepting connections, new connections fail |
| WAL needs a new segment first | PANIC on pg_wal/xlogtemp | Crashes, then starts recovery |
| Recovery with under 16 MB free | FATAL on pg_wal/xlogtemp, then shutting down due to startup process failure | Exit 1. The unless-stopped policy gave 4 restarts in 40 seconds |
What Frees Space on a Full Postgres Disk?
I ran each statement on a volume with 0 bytes free, 3 runs per cell:
| Statement | At zero free | Bytes freed |
|---|---|---|
| SELECT, rows never read since written, checkpoint since | Crash | 0 |
| SELECT, rows already read or vacuumed | Works | 0 |
| DELETE | 1,000 rows work, 10% or all rows crash | 0 |
| VACUUM | could not extend file, or crash | 0 |
| VACUUM FULL | could not extend file, also at 25 MB free | 0 |
| CHECKPOINT | Crash | 0 |
| TRUNCATE, table with indexes | could not extend file | 0, whole table with a little room |
| TRUNCATE, no indexes, no TOAST | Works | Whole table |
| DROP TABLE | Works | Whole table, immediately |
DELETE frees 0 bytes because the file keeps its size, and a large one writes enough WAL to crash the server.
PostgreSQL 18 turns data checksums on by default (read November 7, 2026), and with checksums on, the first read of rows nobody has read since they were written, with a checkpoint since, writes a full page image per page to WAL. A first read of a 163 MB table wrote 164 MB of WAL, and it crashed the server 3 of 3 at zero free and with 64 MB free. Rows already read or vacuumed wrote 0 bytes and worked 6 of 6, so the realistic trigger is a recent bulk import or restore.
TRUNCATE rebuilds each index empty, and the new index file needs a block.
Use DROP TABLE only on a table you have already agreed to lose:
docker exec postgres psql -U postgres -d shop -c 'DROP TABLE scratch_import;'
A scratch restore on the same disk is one way to get here, and DROP DATABASE run first either crashed the server or hit could not write init file.
Drop the big table inside the scratch database, then the database:
docker exec postgres psql -U postgres -d shop_scratch -c 'DROP TABLE orders;'
docker exec postgres psql -U postgres -d shop -c 'DROP DATABASE shop_scratch;'
-d shop_scratch is what makes the first line safe, because orders here is the scratch copy.
The same statement against your production database deletes the production table.
Can You Delete Files in pg_wal?
No. Postgres acknowledges a commit once its WAL record is on disk, while the data pages can still be only in memory. I tried it on disposable volumes:
| Deleted | Next start | Afterwards |
|---|---|---|
| Every segment, or the oldest 7 of 14 | PANIC: could not locate a valid checkpoint record, exit 1 | Refuses to start |
| Newest 7 of 14 | Starts normally, ready to accept connections | 136 of 258 acknowledged batches gone |
The last row starts with no error at all. A separate commit log found 214 of 400 gone, 3 of 3.
A clean pg_amcheck doesn't clear this either.
It exited 0 with default shared_buffers, and with 16 MB it exited 2 and lost rows came back when new commits reused their transaction IDs.
pg_resetwal isn't a recovery step for you here.
It got the first row running in my lab, with a zeroed index page and pg_amcheck exit 2.
The docs (read November 7, 2026) call it a last resort for a server that won't start, followed by an immediate dump and restore.
Did the Data Survive After Recovery?
Yes, in every recovery where WAL stayed untouched:
| Path | Acknowledged vs in table | pg_amcheck | Runs |
|---|---|---|---|
| WAL crash, ballast removed | 219 of 219 | Exit 0 | 3 |
| Data file path, volume grown | 19 of 19, plus 414 of 414 existing | Exit 0 | 3 |
| Inactive slot or failing archive | 15 to 38 per run, all present | Exit 0 | 6 |
How Do You Get a Warning Before Postgres Fills the Disk?
Run these only with room, once the server is healthy:
fallocate -l 64M /mnt/pg/ballast
df --output=pcent /mnt/pg | tail -1
The ballast only gets you back in. I sized 64 MB to a 512 MB disk, so scale it to yours. For an alert threshold, divide free MB by MB of growth per minute.
max_slot_wal_keep_size caps the WAL a slot can hold:
docker exec postgres psql -U postgres -c "ALTER SYSTEM SET max_slot_wal_keep_size = '128MB';"
docker exec postgres psql -U postgres -c 'SELECT pg_reload_conf();'
It's off by default (read November 7, 2026), and a live but slow replica behind the limit can lose its slot, so size 128MB to your disk and replicas.
It applies at checkpoint time, so with max_wal_size at its default 1 GB, larger than my disk, the server crashed first, 3 of 3.
Summary
Free space outside Postgres until df shows more than 16 MB under Avail, and leave pg_wal alone.
Then find the slot or archive that holds WAL, fix it, and only then run CHECKPOINT and pg_amcheck, because both crashed a server that had too little room.
Every acknowledged commit survived when WAL stayed untouched, in process crashes on WSL2, so a real power failure can behave differently.
My self-hosting course practices restoring a backup into a temporary database and builds a monthly maintenance routine that covers server resources.
Thanks for reading.
And stay awesome!
Frequently Asked Questions
What happens when Postgres runs out of disk space?
It depends on what needs space first. If a data file does, writes fail with an error and the server stays up until the next checkpoint crashes it. If WAL needs a new 16 MB segment, the server crashes at once, and recovery exits again with under 16 MB free.
What can I delete when the Postgres disk is full?
Start with anything outside Postgres, such as container logs, old images, or a stray dump. Inside Postgres, DROP TABLE on a table you have agreed to lose freed space at once in testing. DELETE and VACUUM freed 0 bytes, and large deletes crashed the server.
Can I delete files in the pg_wal folder?
No. Deleting the oldest segments stopped the next start with a PANIC. Deleting the newest let the server start normally with 53 percent of acknowledged batches gone. The cause is usually a replication slot or a failing archive, so fix that and let Postgres remove the files.
Is my data intact after Postgres crashes on a full disk?
In my lab, yes. Every acknowledged commit survived every recovery in which WAL was left alone, and pg_amcheck exited 0 where it ran. These were process crashes in a container on WSL2, so a real power failure and a disk write cache were not tested.
Why does Postgres keep restarting after the disk filled?
Recovery has to write a new 16 MB WAL segment. With 8 or 12 MB free it replayed WAL, failed on that write, and exited with code 1, and the restart policy repeated the cycle. With more than 16 MB free it started.
Does VACUUM FULL free space on a full disk?
No. It needs room to write first, so it failed with could not extend file even with 25 MB free. Plain VACUUM freed 0 bytes as well, and it can crash a full server.

