Introduction
A PostgreSQL administrator may occasionally discover that the pg_wal directory on the primary database server has grown to many gigabytes while the standby server contains only relatively recent WAL files.
A common reaction is:
Can I delete the old WAL files from the primary?
The answer is no — never manually delete WAL files from the active PostgreSQL pg_wal directory.
A large pg_wal directory is normally a symptom of another condition. PostgreSQL may be retaining WAL because of WAL archiving, replication slots, standby lag, checkpoint requirements, or configuration settings.
The correct solution is therefore to determine why PostgreSQL is retaining WAL and fix that cause. PostgreSQL can then safely recycle or remove the files itself.
This article uses PostgreSQL 15 and is also relevant to environments using EnterpriseDB Failover Manager (EFM) with PostgreSQL streaming replication.
What Is pg_wal?
PostgreSQL uses Write-Ahead Logging, or WAL, to ensure database durability and crash recovery.
Before PostgreSQL changes a database page on disk, information describing the change is written to WAL.
WAL is also fundamental to:
crash recovery
physical streaming replication
point-in-time recovery
continuous archiving
backup and restore
standby promotion
high-availability architectures
WAL files are stored under:
$PGDATA/pg_wal
A standard PostgreSQL installation normally uses 16 MB WAL segments, although a different WAL segment size can be chosen when a cluster is initially created.
A typical WAL filename looks similar to:
0000000D000000050000003A
You may also see files such as:
0000000D.history
0000000D0000000000000029.partial
000000010000000000000002.00000028.backup
Timeline history files are generally tiny and are not normally responsible for large pg_wal usage.
The large disk consumers are usually the normal WAL segment files.
Is It Normal for the Primary and Standby to Have Different pg_wal Sizes?
Yes.
The primary and standby servers do not need to contain identical sets of WAL files.
For example:
Primary
pg_wal = 15 GB
Standby
pg_wal = 1 GB
does not automatically indicate broken replication.
The standby can recycle WAL after it has been received, replayed and is no longer required for recovery.
The primary may need to retain WAL for additional reasons such as:
PRIMARY
|
pg_wal
|
+---------------+---------------+
| | |
v v v
Standby WAL Archive Replication Slot
Replication
Therefore, the first task is not comparing directory sizes.
The first task is determining whether replication itself is healthy.
Step 1: Check Streaming Replication on the Primary
Run the following query on the PostgreSQL primary:
SELECT
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)
) AS send_lag_bytes,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn)
) AS write_lag_bytes,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn)
) AS flush_lag_bytes,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
) AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
A healthy standby should normally show something similar to:
application_name | state | sync_state
-----------------+-----------+-----------
walreceiver | streaming | sync
Important columns include:
| Column | Meaning |
|---|---|
state | Current WAL sender state |
sent_lsn | WAL sent by the primary |
write_lsn | WAL written on the standby |
flush_lsn | WAL flushed to disk on the standby |
replay_lsn | WAL replayed on the standby |
sync_state | Whether the standby is async, potential, quorum or synchronous |
replay_lag | Delay before WAL changes are replayed |
A server showing:
state = streaming
with very small differences between the LSN positions is normally keeping up well.
LSNs — Log Sequence Numbers — identify positions within PostgreSQL WAL and can be compared to calculate the amount of WAL separating two replication positions.
Step 2: Verify Replication from the Standby
On the standby, first confirm that the server is actually operating in recovery mode:
SELECT pg_is_in_recovery();
Expected result:
t
Then inspect the WAL receiver:
SELECT
status,
sender_host,
sender_port,
slot_name,
written_lsn,
flushed_lsn,
latest_end_lsn,
latest_end_time
FROM pg_stat_wal_receiver;
A healthy streaming standby should normally show:
status = streaming
You can also compare the WAL received and replayed:
SELECT
pg_last_wal_receive_lsn() AS received_lsn,
pg_last_wal_replay_lsn() AS replayed_lsn,
pg_size_pretty(
pg_wal_lsn_diff(
pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn()
)
) AS receive_replay_gap,
now() - pg_last_xact_replay_timestamp() AS replay_delay;
A small and relatively stable receive/replay gap indicates that the standby is keeping up with the primary.
Step 3: Check Whether Replication Is Synchronous
A PostgreSQL environment can have perfectly healthy replication while still having the possibility of losing recently committed transactions during a sudden primary failure.
The distinction is whether replication is synchronous or asynchronous.
Run:
SHOW synchronous_standby_names;
Then:
SHOW synchronous_commit;
And:
SELECT
application_name,
state,
sync_state
FROM pg_stat_replication;
For example:
application_name | state | sync_state
-----------------+-----------+-----------
walreceiver | streaming | sync
indicates that PostgreSQL currently regards that server as a synchronous standby.
In synchronous replication, transaction commit behaviour depends on synchronous_commit and the configured synchronous standby arrangement.
This is an important distinction in an EFM environment:
EFM manages database availability, monitoring, promotion and failover, while PostgreSQL streaming replication is responsible for transporting WAL between the servers.
Do not interpret an EFM cluster being healthy as sufficient evidence by itself that the environment has zero transaction-loss exposure.
Step 4: Determine the Actual Size of pg_wal
Instead of using operating-system commands alone, PostgreSQL 15 provides functions that can inspect the WAL directory.
Run:
SELECT
pg_size_pretty(SUM(size)) AS pg_wal_size,
COUNT(*) AS wal_files
FROM pg_ls_waldir();
To examine older WAL-related files:
SELECT
name,
pg_size_pretty(size) AS size,
modification
FROM pg_ls_waldir()
ORDER BY modification
LIMIT 30;
To count normal 16 MB WAL segments:
SELECT
COUNT(*) AS wal_segments,
pg_size_pretty(SUM(size)) AS total_size,
MIN(modification) AS oldest_wal,
MAX(modification) AS newest_wal
FROM pg_ls_waldir()
WHERE size = 16 * 1024 * 1024;
Remember:
1 WAL segment = approximately 16 MB
1,000 WAL segments ≈ 15.6 GB
for a PostgreSQL cluster using the standard 16 MB WAL segment size.
Step 5: Check wal_keep_size
Run:
SHOW wal_keep_size;
For example:
wal_keep_size
-------------
0
means PostgreSQL is not deliberately keeping an additional fixed minimum amount of historical WAL specifically for standby streaming.
However, this does not mean WAL cannot grow.
PostgreSQL documentation makes clear that wal_keep_size controls only a minimum amount retained for standby purposes. PostgreSQL can retain considerably more WAL when required by archiving, replication slots or checkpoint processing.
Step 6: Check Replication Slots
Replication slots are one of the most common causes of unexpected pg_wal growth.
Run:
SELECT
slot_name,
slot_type,
active,
active_pid,
restart_lsn,
wal_status,
safe_wal_size,
pg_size_pretty(
pg_wal_lsn_diff(
pg_current_wal_lsn(),
restart_lsn
)
) AS retained_wal
FROM pg_replication_slots
ORDER BY restart_lsn;
You could find something similar to:
slot_name | active | wal_status | retained_wal
------------+--------+------------+-------------
standby01 | t | reserved | 64 MB
oldstandby | f | extended | 14 GB
The second slot would deserve investigation.
restart_lsn identifies the oldest WAL position that may still be required by the replication slot's consumer. PostgreSQL will therefore retain the required WAL rather than recycling it.
An abandoned slot can consequently cause pg_wal to grow dramatically.
Check:
SHOW max_slot_wal_keep_size;
The default value of:
-1
means replication slots can retain an unlimited amount of WAL.
Safely Removing an Obsolete Replication Slot
If you positively identify an inactive slot as belonging to a retired or permanently removed standby, it can be dropped:
SELECT pg_drop_replication_slot('old_standby');
However, never drop a slot simply because it is retaining WAL.
First establish:
which server or application owns it
whether the standby is temporarily offline
whether backup software uses it
whether logical replication uses it
whether the system will need the retained WAL later
Dropping a required slot may mean that an offline standby can no longer resume replication without obtaining missing WAL from another source or being rebuilt.
Step 7: Check WAL Archiving
Another major cause of pg_wal growth is WAL archiving.
Run:
SHOW archive_mode;
Then:
SHOW archive_command;
And PostgreSQL 15 environments should also check:
SHOW archive_library;
Now inspect the archiver:
SELECT
archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time,
stats_reset
FROM pg_stat_archiver;
A Particularly Important Configuration Problem
Consider this configuration:
archive_mode = on
archive_command = ''
archive_library = ''
At first glance administrators may assume:
There is no archive command, so PostgreSQL isn't archiving and therefore it shouldn't retain anything.
That assumption is incorrect.
In PostgreSQL 15, if archive_mode is enabled while both the usable archive mechanism and archive command are effectively absent, PostgreSQL treats archiving as temporarily disabled but continues retaining completed WAL segments because it expects archiving to become available.
The PostgreSQL 15 documentation specifically states that when archive_command is empty while archive_mode is enabled and no archive library is configured, WAL archiving is temporarily disabled while WAL segments continue to accumulate.
This can result in:
Primary pg_wal
1 GB
|
3 GB
|
8 GB
|
15 GB
|
30 GB
|
Disk full
even though streaming replication itself is perfectly healthy.
Step 8: Examine the Archive Status Directory
Run:
SELECT
COUNT(*) FILTER (
WHERE name LIKE '%.ready'
) AS ready_files,
COUNT(*) FILTER (
WHERE name LIKE '%.done'
) AS done_files
FROM pg_ls_archive_statusdir();
A large number of .ready files means PostgreSQL has WAL segments that are waiting to be archived.
You can inspect the oldest ones:
SELECT
name,
modification
FROM pg_ls_archive_statusdir()
WHERE name LIKE '%.ready'
ORDER BY modification
LIMIT 30;
Conceptually:
WAL generated
|
v
WAL segment completed
|
v
archive_status/*.ready
|
+----> archive succeeds
|
v
*.done
|
v
WAL becomes eligible
for normal recycling
When the archive process cannot complete successfully, old WAL cannot simply be recycled.
PostgreSQL documentation confirms that when WAL archiving cannot keep pace, or the configured archive command/library repeatedly fails, old WAL can accumulate in pg_wal.
Step 9: Why max_wal_size = 1GB Does Not Mean pg_wal Is Limited to 1 GB
This is one of the most common PostgreSQL WAL misunderstandings.
Suppose:
SHOW max_wal_size;
returns:
1GB
but:
pg_wal = 15GB
This is not necessarily abnormal behaviour.
max_wal_size is not a hard disk-space limit.
PostgreSQL documentation explicitly notes that max_wal_size can be exceeded and should not be considered a hard limit. WAL archiving requirements, replication slots, checkpoint timing and other conditions can cause considerably more WAL to remain in pg_wal.
Therefore:
max_wal_size = 1GB
does not mean:
pg_wal can never exceed 1GB
Example Troubleshooting Scenario
Consider this PostgreSQL 15 configuration:
Streaming replication = healthy
Standby state = streaming
sync_state = sync
Replication lag = a few milliseconds
wal_keep_size = 0
max_wal_size = 1GB
Replication slots = none
archive_mode = on
archive_command = ''
archive_library = ''
pg_wal = >15GB
The standby may be completely healthy.
The replication configuration is not holding the WAL.
Instead:
Transactions
|
v
PRIMARY
|
+-------------------> Synchronous standby
| |
| +---- Healthy
|
v
Completed WAL
|
v
Archiving required
because archive_mode=on
|
v
No archive command/library
|
X
|
PostgreSQL retains WAL
|
v
pg_wal continues growing
In this case the issue is WAL archival configuration, not streaming replication.
How to Safely Fix the Problem
There are two fundamentally different solutions.
The correct choice depends on whether your recovery strategy requires WAL archives.
Option 1: You Require Point-in-Time Recovery or WAL Archiving
If WAL archives are required for:
point-in-time recovery
backup software
disaster recovery
offsite recovery
retention requirements
recovery beyond the available standby
do not disable archive_mode.
Configure a valid archive destination instead.
A simplified local filesystem example could look like:
archive_mode = on
archive_command = 'test ! -f /postgres/archive/%f && cp %p /postgres/archive/%f'
Where:
%p = complete path to the WAL file
%f = WAL filename
The archive destination must have adequate disk capacity and appropriate ownership and permissions.
For production databases, organisations commonly integrate WAL archival with dedicated PostgreSQL backup solutions rather than relying solely on a basic cp command.
After configuring the command:
SELECT pg_reload_conf();
Then verify:
SHOW archive_command;
Monitor:
SELECT
archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time
FROM pg_stat_archiver;
You should eventually see:
archived_count increasing
last_archived_wal populated
last_archived_time recent
The .ready backlog should also begin decreasing:
SELECT
COUNT(*) FILTER (
WHERE name LIKE '%.ready'
) AS waiting,
COUNT(*) FILTER (
WHERE name LIKE '%.done'
) AS completed
FROM pg_ls_archive_statusdir();
Once PostgreSQL has successfully archived the required WAL, it can recycle or remove older segments as part of normal operation.
Option 2: You Do Not Require WAL Archiving
Suppose your architecture intentionally uses only:
PostgreSQL Primary
|
|
Streaming Replication
|
v
PostgreSQL Standby
|
EFM
and your backup/recovery strategy does not require continuous WAL archiving.
Then this configuration:
archive_mode = on
archive_command = ''
is generally inappropriate.
You can instead configure:
archive_mode = off
Before changing it, however, confirm with the backup and infrastructure teams that no recovery solution depends on WAL archiving.
Check:
SHOW config_file;
Then modify the correct PostgreSQL configuration.
For example:
archive_mode = off
archive_mode can only be changed at PostgreSQL server startup, so changing this setting requires a controlled database restart.
After restart:
SHOW archive_mode;
Expected result:
off
Then immediately verify that streaming replication has re-established:
SELECT
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn
FROM pg_stat_replication;
Confirm:
state = streaming
and, where synchronous replication is intended:
sync_state = sync
Should You Run CHECKPOINT?
Once the condition preventing WAL recycling has been corrected, PostgreSQL will normally clean up or recycle eligible WAL itself.
You can monitor the size with:
SELECT
pg_size_pretty(SUM(size)) AS pg_wal_size,
COUNT(*) AS wal_files
FROM pg_ls_waldir();
If necessary after correcting the underlying issue, a manual checkpoint can be performed:
CHECKPOINT;
Then check again:
SELECT
pg_size_pretty(SUM(size)) AS pg_wal_size,
COUNT(*) AS wal_files
FROM pg_ls_waldir();
However, do not repeatedly issue manual checkpoints on a busy production server. Checkpoints can create substantial storage I/O.
The underlying retention problem should always be fixed first.
Never Delete Files Manually from pg_wal
The most important rule in this entire article is:
Never manually delete WAL files from an active PostgreSQL pg_wal directory.
Do not run commands such as:
rm -f $PGDATA/pg_wal/000000*
Do not run:
find $PGDATA/pg_wal -mtime +1 -delete
Do not attempt to decide that a WAL file is "old enough" based only on its filesystem modification date.
PostgreSQL's WAL retention decisions depend on database recovery state, checkpoints, replicas, archiving and other internal requirements.
Removing a WAL file PostgreSQL still requires can result in:
replication failure
inability to restart a standby
broken recovery
failed point-in-time recovery
database startup problems
loss of recovery capability
Should pg_archivecleanup Be Used Against pg_wal?
No.
pg_archivecleanup is intended to clean an archive location when appropriate.
It is not a tool for manually pruning PostgreSQL's active:
$PGDATA/pg_wal
directory.
Let PostgreSQL manage its active WAL directory.
What If the WAL Disk Is Almost Full?
If the filesystem containing pg_wal is approaching critical capacity, do not solve the immediate problem by deleting WAL.
A safer emergency process is:
pg_wal disk filling
|
v
Check replication
|
+---- replication slot retaining WAL?
|
+---- archive backlog?
|
+---- archive destination unavailable?
|
+---- standby severely delayed?
|
+---- unusually high WAL generation?
|
v
Correct underlying cause
|
v
Allow PostgreSQL to recycle WAL
If there is insufficient time to complete the investigation before the filesystem reaches 100%, temporarily increasing disk capacity is generally safer than manually deleting WAL.
A full WAL filesystem can itself result in database availability problems, so proactive monitoring is essential.
Useful PostgreSQL 15 WAL Diagnostic Script
The following queries provide a useful first-level diagnostic set.
Replication
SELECT
application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
pg_size_pretty(
pg_wal_lsn_diff(
pg_current_wal_lsn(),
replay_lsn
)
) AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
Replication Slots
SELECT
slot_name,
slot_type,
active,
active_pid,
restart_lsn,
wal_status,
safe_wal_size,
pg_size_pretty(
pg_wal_lsn_diff(
pg_current_wal_lsn(),
restart_lsn
)
) AS retained_wal
FROM pg_replication_slots;
WAL Configuration
SELECT
name,
setting,
unit,
source,
sourcefile
FROM pg_settings
WHERE name IN (
'wal_level',
'wal_keep_size',
'min_wal_size',
'max_wal_size',
'max_slot_wal_keep_size',
'archive_mode',
'archive_command',
'archive_library',
'synchronous_standby_names',
'synchronous_commit'
)
ORDER BY name;
Archiver Status
SELECT
archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time,
stats_reset
FROM pg_stat_archiver;
Current pg_wal Size
SELECT
pg_size_pretty(SUM(size)) AS pg_wal_size,
COUNT(*) AS wal_file_count
FROM pg_ls_waldir();
WAL Archive Backlog
SELECT
COUNT(*) FILTER (
WHERE name LIKE '%.ready'
) AS waiting_to_archive,
COUNT(*) FILTER (
WHERE name LIKE '%.done'
) AS archived
FROM pg_ls_archive_statusdir();
Oldest and Newest WAL
SELECT
COUNT(*) AS wal_segments,
pg_size_pretty(SUM(size)) AS wal_size,
MIN(modification) AS oldest_wal,
MAX(modification) AS newest_wal
FROM pg_ls_waldir()
WHERE size = 16 * 1024 * 1024;
Diagnostic Decision Tree
Use the following sequence when troubleshooting unusually large pg_wal usage:
pg_wal is large
|
v
Is standby streaming?
/ \
NO YES
| |
Fix replication v
Check replication lag
|
v
Check replication slots
/ \
Old/stale None
slot |
| v
Investigate/drop Check archiving
obsolete slot |
+------+------+
| |
Archive OK Archive stuck/
| not configured
| |
v v
Check WAL Fix archive
generation configuration
|
v
PostgreSQL safely
recycles WAL
PostgreSQL EFM Considerations
In an EnterpriseDB Failover Manager environment, EFM and PostgreSQL streaming replication serve different purposes.
EFM is responsible for high-availability coordination such as:
monitoring database nodes
detecting failures
deciding when failover should occur
promoting a standby
coordinating cluster state
PostgreSQL streaming replication is responsible for moving WAL between primary and standby databases.
Therefore, when diagnosing a large pg_wal, check PostgreSQL's native replication state rather than relying only on the EFM cluster status.
Useful checks include:
SELECT * FROM pg_stat_replication;
and on the standby:
SELECT * FROM pg_stat_wal_receiver;
along with the appropriate EFM cluster-status command for your installation.
Preventing pg_wal Disk Space Problems
Production PostgreSQL environments should monitor more than just database tablespace usage.
Consider monitoring:
pg_walfilesystem usageWAL generation rate
WAL archive failures
number of
.readyfilesreplication lag
standby connectivity
inactive replication slots
WAL retained by each replication slot
archive destination capacity
checkpoint frequency
For replication slots specifically, monitoring restart_lsn is valuable because it identifies how far behind a consumer is and therefore how much WAL PostgreSQL may need to retain. PostgreSQL 15 exposes this information through pg_replication_slots.
Key Takeaways
A large pg_wal directory does not automatically mean PostgreSQL replication is broken.
A primary server can contain substantially more WAL than its standby while streaming replication remains completely healthy.
When investigating, check the environment in this order:
Verify
pg_stat_replication.Verify the standby's
pg_stat_wal_receiver.Check send/write/flush/replay LSN positions.
Check whether replication is synchronous or asynchronous.
Check
pg_replication_slots.Check
wal_keep_size.Check
archive_mode,archive_commandandarchive_library.Examine
pg_stat_archiver.Check
.readyfiles inpg_ls_archive_statusdir().Correct the configuration causing WAL retention.
Allow PostgreSQL to recycle or remove WAL automatically.
Most importantly:
Never manually delete WAL files from PostgreSQL's active
pg_waldirectory simply to recover disk space.
If archive_mode = on while no functioning archive command or archive library exists, PostgreSQL 15 can continue accumulating WAL because the database assumes those WAL files still need to be archived.
Fix the underlying archiving, replication-slot or replication problem and allow PostgreSQL to perform WAL cleanup safely.
FAQ
Why does PostgreSQL pg_wal keep growing?
Common causes include failed or unconfigured WAL archiving, inactive replication slots, lagging standbys, large wal_keep_size settings, high WAL generation and checkpoint-related WAL retention.
Can I manually delete old files from pg_wal?
No. Manually deleting WAL files from an active PostgreSQL cluster can break replication and recovery and may cause serious database availability problems.
Why is pg_wal larger than max_wal_size?
max_wal_size is not a hard limit. PostgreSQL may exceed it when WAL must be retained for archiving, replication slots or other recovery requirements.
Does the standby need the same WAL files as the primary?
No. The standby can recycle files it no longer needs while the primary may retain additional WAL for archiving, slots or checkpoint requirements.
Does sync_state = sync mean replication is working?
It indicates that PostgreSQL currently considers the connected standby synchronous. Also examine the WAL sender/receiver state, LSN positions, lag, synchronous_standby_names and synchronous_commit when assessing the actual durability configuration.
Why are there zero archive failures when archive_command is empty?
An empty archive command does not behave like a command that runs and fails. PostgreSQL can instead leave archiving effectively paused while retaining WAL for future archival, which is why failed_count may remain zero even while pg_wal grows.
Should I turn archive_mode off?
Only if continuous WAL archiving is genuinely not part of your backup, PITR or disaster-recovery strategy. If archiving is required, configure and repair the archive mechanism instead.
Does changing archive_mode require a restart?
Yes. In PostgreSQL 15, archive_mode is a server-start parameter.

