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:

ColumnMeaning
stateCurrent WAL sender state
sent_lsnWAL sent by the primary
write_lsnWAL written on the standby
flush_lsnWAL flushed to disk on the standby
replay_lsnWAL replayed on the standby
sync_stateWhether the standby is async, potential, quorum or synchronous
replay_lagDelay 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_wal filesystem usage

  • WAL generation rate

  • WAL archive failures

  • number of .ready files

  • replication 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:

  1. Verify pg_stat_replication.

  2. Verify the standby's pg_stat_wal_receiver.

  3. Check send/write/flush/replay LSN positions.

  4. Check whether replication is synchronous or asynchronous.

  5. Check pg_replication_slots.

  6. Check wal_keep_size.

  7. Check archive_mode, archive_command and archive_library.

  8. Examine pg_stat_archiver.

  9. Check .ready files in pg_ls_archive_statusdir().

  10. Correct the configuration causing WAL retention.

  11. Allow PostgreSQL to recycle or remove WAL automatically.

Most importantly:

Never manually delete WAL files from PostgreSQL's active pg_wal directory 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.