# Postgres Disk Full: What Fails, What Frees Space

> Free space outside Postgres first. DELETE and VACUUM freed nothing, CHECKPOINT crashed the server, and commits survived unless WAL was deleted by hand.

Published: 2026-11-07. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/postgres-disk-full

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:

```bash
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**](https://milanjovanovic.tech/blog/docker-no-space-left-on-device) measured, so rely on the other lines until there's room.

```bash
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**](https://milanjovanovic.tech/blog/docker-no-space-left-on-device) 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:

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```text
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**](https://www.postgresql.org/docs/18/release-18.html) (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:

```bash
docker exec postgres psql -U postgres -d shop -c 'DROP TABLE scratch_import;'
```

A [**scratch restore on the same disk**](https://milanjovanovic.tech/blog/test-postgres-backup-restore) 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:

```bash
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**](https://www.postgresql.org/docs/18/app-pgresetwal.html) (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:

```bash
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:

```bash
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**](https://www.postgresql.org/docs/18/runtime-config-replication.html) (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**](https://milanjovanovic.tech/pragmatic-self-hosting) 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.
