Fix PostgreSQL WAL Files Filling the Disk on a Django Server
Fix PostgreSQL pg_wal growing until the disk is full on a Django database: find inactive replication slots, broken WAL archiving, or high max_wal_size, free space safely without deleting WAL by hand, and add alerts so it does not happen again.
Problem statement
The database disk alert fires, and du shows the culprit is not your tables:
$ sudo du -sh /var/lib/postgresql/17/main/pg_wal
183G /var/lib/postgresql/17/main/pg_wal
When the disk fills, PostgreSQL stops accepting writes and may shut down with PANIC: could not write to file "pg_wal/xlogtemp.1234": No space left on device. Django requests then fail with OperationalError. WAL (write-ahead log) files are normally recycled after each checkpoint. They pile up only when something tells PostgreSQL to keep them: a replication slot nobody reads, a failing archive_command, or settings that keep far more than needed.
- Check replication slots:
SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) FROM pg_replication_slots; - Drop slots that are inactive and no longer needed:
SELECT pg_drop_replication_slot('old_slot'); - Check archiving:
SELECT * FROM pg_stat_archiver;A risingfailed_countmeansarchive_commandis failing. - Never delete files from
pg_walby hand; it can corrupt the database. - Set
max_slot_wal_keep_sizeso a forgotten slot cannot fill the disk again.
Step-by-step solution
1. Look for a stuck replication slot
SELECT slot_name, slot_type, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
A slot with active = false and a large retained value is the usual cause. Typical owners: a replica that was deleted without dropping its slot, a logical replication subscription from a finished migration, or a change data capture tool (Debezium, Airbyte) that stopped.
2. Drop slots that are no longer used
Confirm nobody needs the slot, then drop it:
SELECT pg_drop_replication_slot('migration_to_new_server');
PostgreSQL removes the retained WAL at the next checkpoint. Run CHECKPOINT; to make it happen now. If the slot belongs to a live replica that is just behind, fix the replica instead; dropping its slot forces you to rebuild it.
3. Fix a failing archive command
With archive_mode = on, PostgreSQL keeps every WAL segment until archive_command reports success:
SELECT archived_count, failed_count, last_failed_wal, last_failed_time
FROM pg_stat_archiver;
Look at the database log for the command's error, such as expired cloud credentials for WAL-G or pgBackRest, a full backup bucket, or a missing directory. Once the command succeeds again, the backlog archives and is recycled. If you no longer need archiving, set archive_mode = off (requires a restart), but understand that this ends point-in-time recovery.
4. Free space in an emergency
If the disk is already full and PostgreSQL is down:
- Free other space on the volume first: old logs, core dumps, package caches.
- Grow the volume (cloud disks can usually be resized online), then extend the file system.
- Start PostgreSQL, then drop the slot or fix archiving as above.
Do not delete files in pg_wal, and do not use pg_resetwal unless you are restoring from backup anyway. Both can lose committed data.
5. Prevent a repeat
# postgresql.conf (PostgreSQL 13+)
max_slot_wal_keep_size = 50GB
With this limit, a slot that falls too far behind is invalidated instead of filling the disk. The replica then needs rebuilding, which is a much smaller problem than a stopped primary. Add alerts on disk usage, on pg_replication_slots retained size, and on pg_stat_archiver.failed_count.
Explanation
Every change to the database is written to WAL before it reaches the table files. PostgreSQL normally keeps only enough WAL to recover from a crash since the last checkpoint, bounded by max_wal_size, and recycles the rest. Replication slots and archiving change that rule on purpose: a slot guarantees its consumer can resume from where it stopped, and archiving guarantees each segment reaches the backup store. Both guarantees mean "keep the file until I say so". If the consumer disappears, the files are kept forever.
Edge cases / notes
- Managed databases. RDS and Cloud SQL handle archiving themselves, but a logical replication slot you created can still fill storage. RDS exposes
OldestReplicationSlotLagin CloudWatch. - Bulk loads. Large imports or index builds generate WAL quickly. Spread them out or temporarily raise disk headroom.
- Separate volume. Putting
pg_walon its own volume protects table data, but the database still stops when that volume fills.
Next steps
- For other disk-space causes, see Fix Django Server Disk Full.
- Make sure archiving actually gives you recovery with PostgreSQL Point-in-Time Recovery for Django.
- Clean up after a move with Migrate PostgreSQL With Logical Replication.
FAQ
Can I delete old files from pg_wal?
No. PostgreSQL may still need them for crash recovery or replicas, and removing them can corrupt the cluster. Remove the reason they are retained instead.
Why did WAL grow after my database migration finished?
Logical replication slots created for the migration stay until dropped. Drop the slot on the old server once the move is complete.
Does VACUUM FULL free WAL space?
No. It rewrites tables and generates more WAL. It does nothing for retained segments.
What size should max_slot_wal_keep_size be?
Enough to cover a replica's normal maintenance downtime, while leaving free disk. Many teams use 20 to 30 percent of the volume.