Loading content...
Every transaction that modifies your database leaves a trace in the Write-Ahead Log (WAL). Under normal operation, these logs are temporary—the database recycles old log segments once checkpoint processing confirms the changes are safely written to data files. But for Point-in-Time Recovery, those 'temporary' logs are the only bridge between your last backup and any recovery target.
Log archiving is the process of preserving that bridge.
Without proper log archiving, PITR capability exists only in theory. The moment a log segment is recycled before archiving, a gap appears in your recovery timeline—a permanent blind spot where the database's history is lost forever.
This page examines log archiving comprehensively: how it works, what can go wrong, how to monitor it, and the operational practices that ensure your PITR capability remains intact through the daily operation of your database systems.
Log archiving is the single point of failure for PITR. A missed archive, a corrupted segment, or a gap in the log sequence can render days of PITR capability useless. This page will teach you how to prevent, detect, and respond to archiving failures.
Before we can properly configure log archiving, we must understand the complete lifecycle of Write-Ahead Log segments. This lifecycle determines when archiving must occur and what happens if it doesn't.
WAL segments progress through several distinct stages:
1. Creation (Allocation) When the current WAL segment fills up, the database allocates a new segment. This may involve:
2. Active Writing The segment receives new log records as transactions execute:
wal_sync_method parameter controls durability guarantees3. Full Status When a segment reaches capacity:
4. Archival Pending The complete segment waits for archive processing:
5. Archived and Retained After successful archival:
6. Recycled The physical file is reassigned a new name and reused:
If archive storage fills up or the archive command fails repeatedly, the database faces a dilemma: stop accepting new transactions (preserving logs but halting operations) or recycle unarchived segments (continuing operations but losing PITR capability). The archive_mode and archive_command configuration controls this behavior.
Proper archive configuration is foundational to reliable PITR. The specific parameters vary by database system, but the concepts are universal. We'll use PostgreSQL's terminology as a reference model, then map to other systems.
archive_mode
Controls whether WAL archiving is enabled:
off: No archiving (default)on: Enable archivingalways: Enable archiving even on standby servers-- Enable archiving (requires restart)
alter system set archive_mode = 'on';
archive_command
The shell command executed to archive each WAL segment:
%p — Path to the WAL segment to archive%f — Filename of the WAL segment-- Basic local archive
alter system set archive_command =
'cp %p /archive/wal/%f';
-- Archive to S3 with compression
alter system set archive_command =
'gzip -c %p | aws s3 cp - s3://db-archive/wal/%f.gz';
-- Archive with verification
alter system set archive_command =
'test ! -f /archive/wal/%f && cp %p /archive/wal/%f';
archive_timeout
Forces an archive switch even if the current segment isn't full:
-- Archive at least every 5 minutes
alter system set archive_timeout = '300s';
| Concept | PostgreSQL | MySQL/MariaDB | Oracle | SQL Server |
|---|---|---|---|---|
| Enable Archiving | archive_mode = on | log_bin = ON | ALTER DATABASE ARCHIVELOG | Full/Bulk-Logged Recovery |
| Archive Command | archive_command | N/A (binlog rotation) | LOG_ARCHIVE_DEST_n | Backup Log with COPY_ONLY |
| Archive Timeout | archive_timeout | expire_logs_days | ARCHIVE_LAG_TARGET | Log backup frequency |
| Archive Location | Command parameter | binlog directory | LOG_ARCHIVE_DEST | Backup destination |
The archive command must satisfy specific requirements to ensure reliable operation:
1. Atomic Success/Failure The command must exit with status 0 only if the archive succeeded completely. Any non-zero exit prevents recycling of the segment.
2. Idempotency The command must succeed if called multiple times for the same file (the segment may already exist in the archive from a previous interrupted attempt).
3. Verification The command should verify the archived copy is complete and uncorrupted before exiting successfully.
4. Destination Check The command should fail if the destination is unreachable, full, or experiencing issues.
5. No Modification of Source The command must not modify or delete the source WAL file.
6. Reasonable Timeout The command should complete in reasonable time (usually seconds). Long-running commands block archival of subsequent segments.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
#!/bin/bash# Robust WAL archiving script with verification# Usage: archive_wal.sh <source_path> <filename> SOURCE_PATH="$1"FILENAME="$2"ARCHIVE_DIR="/archive/wal"S3_BUCKET="s3://db-archive/wal" # Function to log with timestamplog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> /var/log/wal_archive.log} # Check source file existsif [ ! -f "$SOURCE_PATH" ]; then log "ERROR: Source file not found: $SOURCE_PATH" exit 1fi # Calculate source checksumSOURCE_CHECKSUM=$(sha256sum "$SOURCE_PATH" | awk '{print $1}') # Archive to local storage first (fast, reliable)LOCAL_DEST="$ARCHIVE_DIR/$FILENAME"if [ -f "$LOCAL_DEST" ]; then # File exists - verify it matches (idempotency) EXISTING_CHECKSUM=$(sha256sum "$LOCAL_DEST" | awk '{print $1}') if [ "$SOURCE_CHECKSUM" = "$EXISTING_CHECKSUM" ]; then log "INFO: $FILENAME already archived correctly (idempotent success)" exit 0 else log "ERROR: $FILENAME exists but checksum mismatch!" exit 1 fifi # Copy to local archive with fsynccp "$SOURCE_PATH" "$LOCAL_DEST.tmp" && sync "$LOCAL_DEST.tmp"if [ $? -ne 0 ]; then log "ERROR: Failed to copy $FILENAME to local archive" rm -f "$LOCAL_DEST.tmp" exit 1fi # Verify local copyLOCAL_CHECKSUM=$(sha256sum "$LOCAL_DEST.tmp" | awk '{print $1}')if [ "$SOURCE_CHECKSUM" != "$LOCAL_CHECKSUM" ]; then log "ERROR: Checksum mismatch after local copy for $FILENAME" rm -f "$LOCAL_DEST.tmp" exit 1fi # Atomic rename to final locationmv "$LOCAL_DEST.tmp" "$LOCAL_DEST"if [ $? -ne 0 ]; then log "ERROR: Failed to rename $FILENAME in local archive" exit 1fi log "INFO: Successfully archived $FILENAME to local storage" # Async upload to S3 (non-blocking for archive command)# This is handled by a separate process that monitors local archivenohup aws s3 cp "$LOCAL_DEST" "$S3_BUCKET/$FILENAME" >> /var/log/s3_upload.log 2>&1 & exit 0The choice of archive storage profoundly impacts PITR reliability, recovery speed, and total cost of ownership. A well-designed archive strategy typically combines multiple storage tiers to balance these factors.
Enterprise deployments commonly implement multiple storage tiers:
Hot Tier (Recent Archives)
Warm Tier (Recent History)
Cold Tier (Long-term Archive)
Offsite Tier (Disaster Recovery)
| Tier | Typical Storage | Retention | Access Time | Cost/GB/Month |
|---|---|---|---|---|
| Hot | Local NVMe/SSD | 24-72 hours | < 10ms | $0.15-0.30 |
| Warm | Object Storage (Standard) | 7-30 days | 50-200ms | $0.02-0.03 |
| Cold | Object Storage (IA) | 30-365 days | 1-5 seconds | $0.01-0.015 |
| Archive | Glacier/Deep Archive | Years | Minutes-Hours | $0.001-0.004 |
| Offsite DR | Cross-region replicated | Mirrors warm | Variable | 2x base cost |
WAL files compress effectively—typical compression ratios range from 3:1 to 10:1 depending on workload. Compression strategies must balance storage savings against recovery speed:
Inline Compression
Compress during archival:
archive_command = 'gzip -c %p > /archive/%f.gz'
Deferred Compression
Archive uncompressed, compress later:
Compression Algorithm Selection
| Algorithm | Speed | Ratio | Best For |
|---|---|---|---|
| gzip -1 | Fast | 3-4x | Balance speed/ratio |
| gzip -9 | Slow | 4-5x | Cold tier storage |
| lz4 | Very Fast | 2-3x | Hot tier, fast recovery |
| zstd | Fast | 4-6x | Modern systems, best balance |
| xz | Very Slow | 6-8x | Long-term archive only |
For most modern deployments, zstd (Zstandard) offers the best balance of compression ratio and speed. It approaches gzip's compression ratio while operating at speeds closer to lz4. PostgreSQL 15+ supports native zstd compression.
Archived WAL files contain complete transaction data—including all inserted, updated, and deleted values. Encryption is essential for:
Encryption Approaches:
1. Storage-Level Encryption
2. Client-Side Encryption
gpg -e -r backup@company.com %f3. Database-Native Encryption
Key Management Requirements:
Traditional segment-based archiving introduces latency between transaction commit and archival. For systems requiring near-zero RPO, continuous archiving mechanisms stream changes in real-time.
Segment-Based Archiving
The traditional model archives complete segments:
RPO Implication: Maximum data loss = segment size worth of transactions
With a 16MB segment and high-throughput database, this might be seconds. With a low-activity database, a segment might take hours to fill.
Continuous (Streaming) Archiving
Alternative approach streams WAL records continuously:
Implementation Methods:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
#!/usr/bin/env python3"""Continuous WAL Archive ReceiverReceives streaming WAL records and persists to durable storage""" import asyncioimport hashlibfrom datetime import datetimefrom pathlib import Pathimport aiofilesimport boto3from dataclasses import dataclassfrom typing import Optional @dataclassclass WALRecord: """Represents a single WAL record from the stream""" lsn: str timeline: int xid: Optional[int] timestamp: datetime data: bytes @property def checksum(self) -> str: return hashlib.sha256(self.data).hexdigest()[:16] class StreamingArchiveReceiver: """ Receives continuous WAL stream and archives to multiple storage tiers with configurable durability """ def __init__( self, local_archive_path: Path, s3_bucket: str, buffer_size: int = 1024 * 1024, # 1MB buffer flush_interval_seconds: float = 1.0, sync_to_s3_interval: int = 60 ): self.local_path = local_archive_path self.s3_bucket = s3_bucket self.buffer_size = buffer_size self.flush_interval = flush_interval_seconds self.s3_sync_interval = sync_to_s3_interval self.buffer: list[WALRecord] = [] self.buffer_bytes = 0 self.last_flushed_lsn: Optional[str] = None self.s3_client = boto3.client('s3') async def receive_record(self, record: WALRecord): """Process incoming WAL record from the stream""" self.buffer.append(record) self.buffer_bytes += len(record.data) # Flush buffer when it reaches size threshold if self.buffer_bytes >= self.buffer_size: await self.flush_buffer() async def flush_buffer(self): """Persist buffered records to local storage""" if not self.buffer: return # Determine filename based on LSN range first_lsn = self.buffer[0].lsn last_lsn = self.buffer[-1].lsn timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S') filename = f"wal_stream_{first_lsn}_{last_lsn}_{timestamp}.wal" filepath = self.local_path / filename # Write with fsync for durability async with aiofiles.open(filepath, 'wb') as f: for record in self.buffer: # Write record with length prefix and checksum header = f"{len(record.data)}:{record.checksum}:".encode() await f.write(header + record.data + b'\n') await f.flush() # Force sync to disk import os os.fsync(f.fileno()) # Update tracking self.last_flushed_lsn = last_lsn self.buffer.clear() self.buffer_bytes = 0 print(f"Flushed WAL records to {filename}, " f"last LSN: {last_lsn}") async def run_periodic_tasks(self): """Background tasks for time-based flushing and S3 sync""" while True: await asyncio.sleep(self.flush_interval) # Time-based buffer flush if self.buffer: await self.flush_buffer()PostgreSQL provides pg_receivewal for streaming archive reception:
# Basic streaming archive
pg_receivewal -h primary-db -U replication -D /archive/wal \
--synchronous --create-slot --slot=archive_receiver
# With compression
pg_receivewal -h primary-db -U replication -D /archive/wal \
--compress=zstd:3 --synchronous
Advantages of pg_receivewal:
Considerations:
Archive failures can silently accumulate, creating PITR gaps that aren't discovered until a recovery is attempted. Proactive monitoring is essential for maintaining PITR capability.
1. Archive Lag
The delay between WAL generation and archival:
-- PostgreSQL: Check archive lag
SELECT
pg_walfile_name(pg_current_wal_lsn()) AS current_wal,
last_archived_wal,
pg_wal_lsn_diff(
pg_current_wal_lsn(),
(last_archived_wal || '000000')::pg_lsn
) / 1024 / 1024 AS lag_mb,
last_archived_time,
now() - last_archived_time AS time_since_last_archive
FROM pg_stat_archiver;
Alert Thresholds:
2. Archive Failure Count
-- PostgreSQL: Check recent archive failures
SELECT
failed_count,
last_failed_wal,
last_failed_time
FROM pg_stat_archiver;
Alert Thresholds:
3. Archive Storage Capacity
Monitor archive destination for space:
# Local archive storage
df -h /archive/wal
# S3 bucket size (via CloudWatch or CLI)
aws s3 ls s3://db-archive/wal/ --summarize
Alert Thresholds:
| Metric | Description | Warning Threshold | Critical Threshold |
|---|---|---|---|
| Archive Lag (segments) | Unarchived complete segments | 10 segments | 50 segments |
| Archive Lag (time) | Time since last successful archive | 5 minutes | 30 minutes |
| Failed Archive Count | Cumulative archive command failures | 0 in 1 hour | Count increasing |
| Archive Storage Free | Available space at destination | < 20% | < 5% |
| Archive Completeness | No gaps in archive sequence | N/A | Any gap detected |
| Archive Command Duration | Time per archive operation | 30 seconds | 120 seconds |
Beyond monitoring active archival, periodic verification ensures archive integrity:
1. Sequence Continuity Check
Verify no gaps exist in the WAL sequence:
#!/bin/bash
# Verify WAL archive sequence continuity
ARCHIVE_DIR="/archive/wal"
prev_segment=""
for file in $(ls -1 $ARCHIVE_DIR/0000*.gz | sort); do
segment=$(basename $file .gz)
if [ -n "$prev_segment" ]; then
expected=$(pg_waldiff $prev_segment)
if [ "$segment" != "$expected" ]; then
echo "GAP DETECTED: Expected $expected after $prev_segment, found $segment"
exit 1
fi
fi
prev_segment=$segment
done
echo "Archive sequence verified: no gaps"
2. Archive Checksum Verification
# Verify archive checksums
for file in /archive/wal/*.gz; do
gunzip -t "$file" 2>/dev/null || echo "CORRUPT: $file"
done
3. Sample Restore Testing
Periodic end-to-end recovery tests:
The most reliable way to ensure PITR works is to actually perform recoveries regularly. Automate weekly or monthly recovery tests to a non-production environment. This validates the complete recovery chain: base backup + archives + recovery process.
Archive retention determines how far back in time PITR can reach. Retention policies must balance recovery capability against storage costs and compliance requirements.
Recovery Window Retention
Archives needed to support PITR within the operational recovery window:
Compliance Retention
Archives required by regulatory frameworks:
Base Backup Correlation
Archives must be retained at least as long as there's a corresponding base backup:
#!/bin/bash
# WAL archive retention automation script
ARCHIVE_DIR="/archive/wal"
HOT_RETENTION_DAYS=3
WARM_RETENTION_DAYS=30
COLD_RETENTION_DAYS=365
# Get current date for comparison
NOW=$(date +%s)
for archive_file in "$ARCHIVE_DIR"/*.gz; do
[ -f "$archive_file" ] || continue
# Get file modification time
FILE_TIME=$(stat -c %Y "$archive_file")
AGE_DAYS=$(( (NOW - FILE_TIME) / 86400 ))
# Determine action based on age
if [ $AGE_DAYS -gt $COLD_RETENTION_DAYS ]; then
# Beyond cold retention: delete
echo "Deleting expired archive: $archive_file (age: $AGE_DAYS days)"
rm "$archive_file"
elif [ $AGE_DAYS -gt $WARM_RETENTION_DAYS ]; then
# Move to cold tier (Glacier)
BASENAME=$(basename "$archive_file")
if ! aws s3api head-object --bucket db-archive-cold \
--key "wal/$BASENAME" 2>/dev/null; then
echo "Moving to cold tier: $archive_file"
aws s3 cp "$archive_file" "s3://db-archive-cold/wal/$BASENAME" \
--storage-class GLACIER
fi
elif [ $AGE_DAYS -gt $HOT_RETENTION_DAYS ]; then
# Move to warm tier (S3 Standard-IA)
BASENAME=$(basename "$archive_file")
if ! aws s3api head-object --bucket db-archive-warm \
--key "wal/$BASENAME" 2>/dev/null; then
echo "Moving to warm tier: $archive_file"
aws s3 cp "$archive_file" "s3://db-archive-warm/wal/$BASENAME" \
--storage-class STANDARD_IA
fi
fi
done
The cardinal rule of archive retention: never delete WAL archives until the oldest base backup depending on them has also been deleted. The sequence must be: delete old base backup → delete WAL archives that only that backup needed → never the reverse.
Even well-configured archive systems encounter problems. Rapid diagnosis and resolution is critical to minimizing PITR gaps.
Problem: Archive Command Hangs
Symptoms:
Diagnosis:
-- Check archiver status
SELECT * FROM pg_stat_archiver;
-- Check PostgreSQL logs
tail -100 /var/log/postgresql/postgresql-*.log | grep -i archive
Common Causes:
Resolution:
# Kill stuck archive command
killall -9 "archive_command_process_name"
# PostgreSQL will retry automatically
# Verify with:
SELECT * FROM pg_stat_archiver;
Problem: Archive Destination Full
Symptoms:
Immediate Actions:
# Emergency space cleanup (carefully!)
# Only delete archives older than oldest needed backup
find /archive/wal -mtime +30 -name "*.gz" -delete
Problem: Archive Gap Discovered
A gap in the archive sequence renders PITR impossible across that gap.
Discovery:
# List archive sequence to find gaps
ls -1 /archive/wal/ | sort | awk '
NR==1 {prev=$1; next}
{
# Check if current file follows previous
# (Implementation depends on WAL naming convention)
if (gap_detected) print "GAP: " prev " -> " $1
prev=$1
}
'
Recovery Options:
Log archiving is the often-overlooked foundation of PITR capability. Let's consolidate the key concepts:
What's next:
With the archive foundation established, we'll examine the Recovery Process itself. The next page walks through the complete PITR recovery workflow—from incident detection through recovery target selection, the actual recovery procedure, and post-recovery validation steps that ensure a successful restoration.
You now understand the critical role of log archiving in PITR. You've learned how WAL segments progress through their lifecycle, how to configure robust archive commands, storage tiering strategies, monitoring requirements, and retention policies. Next, we'll explore how these archived logs are used during the actual recovery process.