Database Backup Strategies: Point-in-Time Recovery
Point-in-time recovery is the ability to restore your database to any specific moment in time, not just to when you last ran a backup. This capability is non-negotiable for production systems where...
Key Insights
- Point-in-time recovery requires both full backups and continuous transaction log archiving—losing either component makes precise recovery impossible
- PostgreSQL’s WAL archiving and MySQL’s binary logs use fundamentally different approaches, but both enable recovery to any timestamp within your retention window
- Cloud-managed databases automate PITR infrastructure, but you still need to test recovery procedures regularly and understand restore time implications for your RTO requirements
Introduction to Point-in-Time Recovery (PITR)
Point-in-time recovery is the ability to restore your database to any specific moment in time, not just to when you last ran a backup. This capability is non-negotiable for production systems where data integrity and business continuity matter.
Traditional full backups create snapshots at discrete intervals—maybe daily at midnight. If someone accidentally deletes critical customer records at 2 PM, a full backup strategy forces you to choose between losing 14 hours of data (restore last night’s backup) or losing the deleted records entirely (keep running with corrupted data). PITR eliminates this false choice by letting you restore to 1:59 PM, one minute before the deletion.
The scenarios demanding PITR are common: a developer runs an UPDATE without a WHERE clause in production, ransomware corrupts tables over several hours before detection, or a buggy application release gradually corrupts data. PITR also enables safer testing by creating production clones at specific timestamps.
The key difference from simple backups: PITR requires continuous capture of all database changes between full backups. This means additional infrastructure, storage costs, and complexity—but the operational safety is worth it.
How PITR Works: Architecture and Components
PITR combines two elements: periodic full backups (base backups) and continuous transaction log archiving. The full backup provides a starting point, while transaction logs record every change made after that backup.
Here’s the conceptual timeline:
Full Backup Transaction Logs Current State
| | | | | | |
|----WAL-001----WAL-002----WAL-003----WAL-004---------|
Sunday Friday
00:00 Any point here can be restored 14:30
When you restore to Wednesday at 10:15 AM, the database:
- Restores the full backup from Sunday
- Replays transaction logs sequentially until reaching Wednesday 10:15 AM
- Stops replay at the target timestamp
Your Recovery Point Objective (RPO) depends on transaction log archive frequency. With continuous archiving, RPO approaches zero—you can recover to within seconds of a failure. Recovery Time Objective (RTO) depends on backup size and how many logs need replay. Restoring a 2TB database and replaying three days of transaction logs might take hours.
Transaction logs go by different names: Write-Ahead Logs (WAL) in PostgreSQL, binary logs (binlogs) in MySQL, redo logs in Oracle. The concept remains consistent: sequential records of every committed transaction.
Implementing PITR in PostgreSQL
PostgreSQL’s WAL archiving is robust and well-documented. Configure continuous archiving by modifying postgresql.conf:
# Enable WAL archiving
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f'
archive_timeout = 300
# Adjust WAL retention
wal_keep_size = 1GB
max_wal_senders = 3
The archive_command executes each time PostgreSQL fills a WAL segment (typically 16MB). This example copies WAL files to a separate mount point. Production systems should archive to object storage like S3:
archive_command = 'aws s3 cp %p s3://my-backup-bucket/wal-archive/%f'
Create base backups using pg_basebackup:
#!/bin/bash
# automated-backup.sh
BACKUP_DIR="/mnt/backups/$(date +%Y%m%d_%H%M%S)"
S3_BUCKET="s3://my-backup-bucket/base-backups"
# Create base backup
pg_basebackup -h localhost -U postgres -D "$BACKUP_DIR" \
-Ft -z -P -X fetch
# Upload to S3
aws s3 sync "$BACKUP_DIR" "$S3_BUCKET/$(basename $BACKUP_DIR)/"
# Cleanup local backups older than 7 days
find /mnt/backups -type d -mtime +7 -exec rm -rf {} +
For recovery, create a recovery.signal file and configure postgresql.conf or use recovery parameters:
# postgresql.conf recovery settings
restore_command = 'cp /mnt/wal_archive/%f %p'
recovery_target_time = '2024-01-15 14:30:00'
recovery_target_action = promote
Start PostgreSQL and it will restore the base backup, replay WAL files until reaching the target time, then promote to a normal running state.
Implementing PITR in MySQL
MySQL uses binary logs differently than PostgreSQL’s WAL. Enable binary logging in my.cnf:
[mysqld]
# Binary log configuration
server-id = 1
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
binlog_expire_logs_seconds = 604800
max_binlog_size = 100M
# Point-in-time recovery support
binlog_row_image = FULL
sync_binlog = 1
Create consistent base backups with mysqldump:
#!/bin/bash
BACKUP_FILE="backup-$(date +%Y%m%d-%H%M%S).sql"
mysqldump --single-transaction \
--master-data=2 \
--flush-logs \
--all-databases \
--routines \
--triggers \
--events \
> "/mnt/backups/$BACKUP_FILE"
gzip "/mnt/backups/$BACKUP_FILE"
The --master-data=2 option records the binary log position in the dump file, crucial for knowing where to start log replay.
To restore to a specific point in time:
# Restore base backup
mysql < backup-20240115-000000.sql
# Replay binary logs to specific timestamp
mysqlbinlog --stop-datetime="2024-01-15 14:30:00" \
/var/log/mysql/mysql-bin.000042 \
/var/log/mysql/mysql-bin.000043 \
| mysql
# Or stop at specific position if you know it
mysqlbinlog --stop-position=12345678 \
/var/log/mysql/mysql-bin.000042 \
| mysql
For large databases, use Percona XtraBackup instead of mysqldump for faster, non-blocking backups:
xtrabackup --backup --target-dir=/mnt/backups/full-backup
xtrabackup --prepare --target-dir=/mnt/backups/full-backup
Cloud-Native PITR Solutions
Managed database services handle PITR infrastructure automatically. AWS RDS, for example, enables point-in-time recovery by default with configurable retention (1-35 days).
Restore an RDS instance to a specific timestamp using AWS CLI:
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier prod-database \
--target-db-instance-identifier prod-database-restored \
--restore-time 2024-01-15T14:30:00Z
# Or restore to latest restorable time
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier prod-database \
--target-db-instance-identifier prod-database-restored \
--use-latest-restorable-time
Google Cloud SQL provides similar functionality:
gcloud sql backups create \
--instance=prod-database \
--description="Pre-deployment backup"
gcloud sql instances clone prod-database prod-database-clone \
--point-in-time '2024-01-15T14:30:00.000Z'
Cloud PITR advantages: automated backup scheduling, encrypted storage, geographic redundancy, and tested restore procedures. Disadvantages: vendor lock-in, limited control over backup storage location, and potential costs for long retention periods.
Critical limitation: cloud PITR creates new instances; you cannot restore in-place. Factor this into RTO calculations—DNS changes, application reconfiguration, and connection string updates add time.
Testing and Validation Strategy
Untested backups are Schrödinger’s backups—simultaneously working and broken until you try restoring. Test recovery procedures monthly at minimum, weekly for critical systems.
Automate backup validation with scripts that restore to a test environment and verify data integrity:
#!/usr/bin/env python3
import subprocess
import datetime
import psycopg2
from datetime import timedelta
def test_pitr_restore():
"""Test PostgreSQL PITR restore and validate data"""
# Calculate restore point (1 hour ago)
restore_time = datetime.datetime.now() - timedelta(hours=1)
# Trigger restore to test instance
print(f"Restoring to {restore_time.isoformat()}")
subprocess.run([
'pg_basebackup', '-h', 'prod-db', '-D', '/tmp/test-restore',
'-U', 'postgres', '-X', 'fetch'
], check=True)
# Start test instance and verify
# (Implementation depends on your infrastructure)
# Connect and validate critical data
conn = psycopg2.connect(
host='test-db',
database='testdb',
user='postgres'
)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM critical_table")
count = cursor.fetchone()[0]
# Compare with expected count
if count < 1000:
raise Exception(f"Data validation failed: only {count} rows")
print(f"✓ Validation passed: {count} rows found")
conn.close()
return True
if __name__ == '__main__':
try:
test_pitr_restore()
print("Backup test SUCCESSFUL")
except Exception as e:
print(f"Backup test FAILED: {e}")
# Send alert via PagerDuty, Slack, etc.
exit(1)
Schedule this via cron or CI/CD pipelines. Track metrics: restore success rate, restore duration, and data validation results. Alert on failures immediately.
Best Practices and Common Pitfalls
Retention policies: Balance compliance requirements against storage costs. Maintain daily backups for 30 days, weekly for 90 days, monthly for a year. Transaction logs must cover your entire retention window.
Storage considerations: Transaction logs accumulate quickly on high-volume databases. A busy PostgreSQL instance might generate 50GB of WAL daily. Budget accordingly and implement lifecycle policies to archive older logs to cheaper storage tiers.
Encryption: Encrypt backups at rest and in transit. Use database-native encryption or storage-level encryption. Never store unencrypted backups in cloud object storage.
Monitoring: Alert on backup failures, archive lag, and disk space. PostgreSQL’s pg_stat_archiver view shows archive status. MySQL’s SHOW MASTER STATUS reveals binary log position.
Common mistakes to avoid:
- Not testing restores: Backups work until you need them. Test regularly.
- Insufficient transaction log retention: Full backups are useless without corresponding logs.
- Ignoring restore time: A 12-hour restore process doesn’t meet a 4-hour RTO.
- Single storage location: Store backups geographically separate from production.
- Missing monitoring: Silent backup failures are discovered during disasters.
PITR isn’t optional for production databases—it’s the minimum viable backup strategy. Implement it correctly, test it relentlessly, and sleep better knowing you can recover from nearly any data disaster.