Skip to content

Retention Policies

backupx provides intelligent retention policies to automatically clean up old backups, preventing storage bloat while ensuring you keep enough backups for recovery needs.

Overview

Retention policies automatically:

  • Delete old backups based on count or age
  • Preserve recent backups for quick recovery
  • Handle mixed backup types (databases and files)
  • Run during backup process for seamless maintenance
  • Provide detailed logging of cleanup operations

Basic Configuration

Count-Based Retention

Keep only the most recent backups:

ts
const config = {
  retention: {
    count: 7, // Keep only the 7 most recent backups
  }
}

Age-Based Retention

Delete backups older than a specified age:

ts
const config = {
  retention: {
    maxAge: 30, // Delete backups older than 30 days
  }
}

Combined Retention

Use both count and age limits together:

ts
const config = {
  retention: {
    count: 10, // Keep at least 10 backups
    maxAge: 90, // But delete anything older than 90 days
  }
}

How Retention Works

Cleanup Process

Retention cleanup happens automatically after each backup:

  1. Scan backup directory for existing backup files
  2. Identify backup files by extension (.sql, .tar, .tar.gz, etc.)
  3. Sort by modification time (newest first)
  4. Apply retention rules (count and/or age)
  5. Delete old backups that exceed limits
  6. Log cleanup results if verbose mode is enabled

File Identification

backupx recognizes backup files by:

  • Extensions: .sql, .tar, .tar.gz, .gz
  • Patterns: Files containing backup-related patterns
  • Timestamps: Files with timestamp patterns in names
  • Age: Based on file modification time

Retention Strategies

Development Environment

Keep few backups for rapid iteration:

ts
const devRetention: BackupConfig = {
  verbose: true,
  outputPath: './dev-backups',

  databases: [
    {
      type: BackupType.SQLITE,
      name: 'dev-db',
      path: './dev.sqlite',
    }
  ],

  files: [
    {
      name: 'src',
      path: './src',
      exclude: ['node_modules/**'],
    }
  ],

  retention: {
    count: 3, // Keep only 3 most recent backups
  }
}

Staging Environment

Balanced approach for testing:

ts
const stagingRetention: BackupConfig = {
  verbose: true,
  outputPath: './staging-backups',

  databases: [
    // ...
  ],
  files: [
    // ...
  ],

  retention: {
    count: 14, // Keep 2 weeks of backups
    maxAge: 30, // Delete anything older than 30 days
  }
}

Production Environment

Conservative approach for production:

ts
const productionRetention: BackupConfig = {
  verbose: false,
  outputPath: '/var/backups/app',

  databases: [
    // ...
  ],
  files: [
    // ...
  ],

  retention: {
    count: 30, // Keep 30 recent backups (1 month daily)
    maxAge: 365, // Keep backups for 1 year
  }
}

Advanced Retention Scenarios

Tiered Retention Strategy

Implement different retention for different backup types:

ts
// Separate configurations for different needs
const configs = {
  // Frequent backups with short retention
  frequent: {
    outputPath: './frequent-backups',
    databases: [
      {
        type: BackupType.SQLITE,
        name: 'cache-db',
        path: './cache.sqlite',
      }
    ],
    retention: {
      count: 5, // Keep only 5 recent
      maxAge: 7, // Delete after 1 week
    }
  },

  // Important backups with long retention
  important: {
    outputPath: './important-backups',
    databases: [
      {
        type: BackupType.POSTGRESQL,
        name: 'main-db',
        connection: process.env.DATABASE_URL!,
      }
    ],
    retention: {
      count: 90, // Keep 90 backups
      maxAge: 730, // Keep for 2 years
    }
  },
}

// Run different backup schedules
async function runTieredBackups() {
  // Frequent backups (hourly)
  if (new Date().getMinutes() === 0) {
    await new BackupManager(configs.frequent).createBackup()
  }

  // Important backups (daily)
  if (new Date().getHours() === 2) {
    await new BackupManager(configs.important).createBackup()
  }
}

Environment-Specific Retention

Adjust retention based on environment:

ts
const retentionPolicies = {
  development: {
    count: 3, // Minimal retention for dev
    maxAge: 7,
  },
  staging: {
    count: 14, // Medium retention for staging
    maxAge: 30,
  },
  production: {
    count: 60, // Long retention for production
    maxAge: 365,
  },
}

const env = process.env.NODE_ENV || 'development'
const retention = retentionPolicies[env as keyof typeof retentionPolicies]

const config: BackupConfig = {
  // ... other configuration
  retention,
}

Size-Based Considerations

Consider backup sizes when setting retention:

ts
// For large backups, keep fewer copies
const largeDatabaseConfig = {
  databases: [
    {
      type: BackupType.POSTGRESQL,
      name: 'analytics-db', // Large database
      connection: process.env.ANALYTICS_DB_URL!,
      compress: true, // Essential for large DBs
    }
  ],
  retention: {
    count: 7, // Fewer copies due to size
    maxAge: 30,
  }
}

// For small backups, keep more copies
const smallConfigFiles = {
  files: [
    {
      name: 'config-files',
      path: './config',
      compress: false, // Small files don't need compression
    }
  ],
  retention: {
    count: 30, // More copies since they're small
    maxAge: 90,
  }
}

Monitoring Retention

Cleanup Logging

With verbose mode enabled, see detailed cleanup information:

🧹 Cleaning up old backups...
   Found 12 backup files
   Keeping 7 most recent files
   Deleting 5 old backup files:
   - old_backup_2023-11-15T10-30-00.sql (18 days old)
   - old_backup_2023-11-14T10-30-00.sql (19 days old)
   - old_backup_2023-11-13T10-30-00.sql (20 days old)
   - old_backup_2023-11-12T10-30-00.sql (21 days old)
   - old_backup_2023-11-11T10-30-00.sql (22 days old)
✅ Cleanup completed: 5 files deleted

Retention Metrics

Track retention effectiveness:

ts
async function trackRetentionMetrics(summary: BackupSummary) {
  const backupDir = './backups'
  const files = await readdir(backupDir)

  const backupFiles = files.filter(file =>
    file.endsWith('.sql')
    || file.endsWith('.tar')
    || file.endsWith('.tar.gz')
    || file.endsWith('.gz')
  )

  const metrics = {
    timestamp: new Date().toISOString(),
    totalBackups: backupFiles.length,
    newBackups: summary.successCount,
    retentionActive: !!config.retention,
    retentionCount: config.retention?.count,
    retentionMaxAge: config.retention?.maxAge,
  }

  // Log metrics for monitoring
  console.log(`📊 Backup metrics:`, metrics)

  // Store for analysis
  fs.appendFileSync('./retention-metrics.jsonl', `${JSON.stringify(metrics)}\n`)
}

Storage Usage Tracking

Monitor how retention affects storage usage:

ts
async function trackStorageUsage(outputPath: string) {
  const files = await readdir(outputPath)
  let totalSize = 0
  let oldestFile = null
  let newestFile = null

  for (const file of files) {
    const filePath = join(outputPath, file)
    const stats = await stat(filePath)

    totalSize += stats.size

    if (!oldestFile || stats.mtime < oldestFile.mtime) {
      oldestFile = { name: file, mtime: stats.mtime }
    }

    if (!newestFile || stats.mtime > newestFile.mtime) {
      newestFile = { name: file, mtime: stats.mtime }
    }
  }

  console.log(`💾 Storage usage:`)
  console.log(`   Total size: ${formatBytes(totalSize)}`)
  console.log(`   Files: ${files.length}`)
  console.log(`   Oldest: ${oldestFile?.name} (${formatDate(oldestFile?.mtime)})`)
  console.log(`   Newest: ${newestFile?.name} (${formatDate(newestFile?.mtime)})`)
}

function formatBytes(bytes: number): string {
  const sizes = ['Bytes', 'KB', 'MB', 'GB']
  if (bytes === 0)
    return '0 Bytes'
  const i = Math.floor(Math.log(bytes) / Math.log(1024))
  return `${Math.round(bytes / 1024 ** i * 100) / 100} ${sizes[i]}`
}

function formatDate(date: Date): string {
  return date.toISOString().split('T')[0]
}

Error Handling

Cleanup Failures

Handle retention cleanup errors gracefully:

ts
// Retention cleanup is non-blocking
const manager = new BackupManager(config)

try {
  const summary = await manager.createBackup()

  // Backup succeeded even if cleanup failed
  if (summary.successCount > 0) {
    console.log('✅ Backups created successfully')
  }

  // Check for cleanup warnings (if any)
  // (backupx logs cleanup errors but doesn't fail the backup)
}
catch (error) {
  console.error('❌ Backup process failed:', error)
}

Manual Cleanup

Implement manual cleanup for maintenance:

ts
import { readdir, stat, unlink } from 'node:fs/promises'
import { join } from 'node:path'

async function manualCleanup(outputPath: string, options: RetentionConfig) {
  const files = await readdir(outputPath)

  // Filter backup files
  const backupFiles = files.filter((file) => {
    return file.endsWith('.sql')
      || file.endsWith('.tar')
      || file.endsWith('.tar.gz')
      || file.endsWith('.gz')
  })

  // Get file stats
  const fileStats = await Promise.all(
    backupFiles.map(async (file) => {
      const filePath = join(outputPath, file)
      const stats = await stat(filePath)
      return { name: file, path: filePath, mtime: stats.mtime }
    })
  )

  // Sort by modification time (newest first)
  fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())

  const filesToDelete = []

  // Apply count-based retention
  if (options.count && fileStats.length > options.count) {
    filesToDelete.push(...fileStats.slice(options.count))
  }

  // Apply age-based retention
  if (options.maxAge) {
    const cutoffDate = new Date()
    cutoffDate.setDate(cutoffDate.getDate() - options.maxAge)

    const oldFiles = fileStats.filter(file => file.mtime < cutoffDate)
    filesToDelete.push(...oldFiles)
  }

  // Remove duplicates
  const uniqueFilesToDelete = Array.from(
    new Map(filesToDelete.map(f => [f.path, f])).values()
  )

  // Delete files
  for (const file of uniqueFilesToDelete) {
    try {
      await unlink(file.path)
      console.log(`🗑️ Deleted: ${file.name}`)
    }
    catch (error) {
      console.error(`❌ Failed to delete ${file.name}:`, error)
    }
  }

  console.log(`✅ Manual cleanup completed: ${uniqueFilesToDelete.length} files deleted`)
}

// Usage
await manualCleanup('./backups', { count: 10, maxAge: 30 })

Best Practices

1. Start Conservative

Begin with longer retention and adjust based on needs:

ts
// Start with conservative settings
const initialRetention = {
  count: 30, // Keep plenty of backups initially
  maxAge: 90, // Long retention period
}

// Monitor and adjust over time
const optimizedRetention = {
  count: 14, // Reduce after understanding patterns
  maxAge: 45, // Adjust based on recovery needs
}

2. Environment Alignment

Match retention to environment importance:

ts
const environmentRetention = {
  // Development: Aggressive cleanup
  dev: { count: 3, maxAge: 7 },

  // Staging: Moderate retention
  staging: { count: 14, maxAge: 30 },

  // Production: Conservative retention
  prod: { count: 60, maxAge: 365 },
}

3. Consider Recovery Patterns

Align retention with how you actually recover:

ts
// If you typically need backups from the last week
const weeklyPattern = {
  count: 7, // One week of daily backups
  maxAge: 14, // Keep extras for 2 weeks
}

// If you need longer history for auditing
const auditPattern = {
  count: 30, // Monthly backups
  maxAge: 365, // Keep for compliance
}

4. Monitor Storage Costs

Balance retention against storage costs:

ts
async function calculateStorageCosts(outputPath: string) {
  const files = await readdir(outputPath)
  let totalSize = 0

  for (const file of files) {
    const stats = await stat(join(outputPath, file))
    totalSize += stats.size
  }

  // Estimate costs (example: $0.023 per GB per month for S3)
  const costPerGBPerMonth = 0.023
  const monthlyCost = (totalSize / (1024 ** 3)) * costPerGBPerMonth

  console.log(`💰 Estimated monthly storage cost: $${monthlyCost.toFixed(2)}`)
  console.log(`💡 Consider adjusting retention if costs are high`)
}

Troubleshooting

Retention Not Working

Check configuration:

ts
// ❌ Problem: No retention configured
const config = {
  // retention property missing
}

// ✅ Solution: Add retention configuration
const config = {
  retention: {
    count: 7,
    maxAge: 30,
  }
}

Verify file patterns:

bash
# Check what files exist in backup directory
ls -la ./backups/

# Check file modification times
ls -lt ./backups/

Files Not Being Deleted

Check file permissions:

bash
# Ensure write permissions on backup directory
chmod 755 ./backups/

# Check individual file permissions
ls -la ./backups/

Manual verification:

ts
// Test retention logic manually
const testConfig = {
  retention: { count: 2 }
}

// Create test files and run backup to see if cleanup works

Next Steps

Retention Configuration

Configure automatic cleanup of old backups:

ts
const retentionConfig = {
  retention: {
    count: 10, // Keep last 10 backups
    maxAge: 30, // Delete backups older than 30 days
  },
}

Count-Based Retention

ts
const countBasedConfig = {
  retention: {
    count: 5, // Keep only the 5 most recent backups
  },
}

Age-Based Retention

ts
const ageBasedConfig = {
  retention: {
    maxAge: 7, // Delete backups older than 7 days
  },
}

For production systems:

ts
const productionRetention = {
  retention: {
    count: 30, // Keep last 30 backups
    maxAge: 90, // But delete anything older than 90 days
  },
}

For development:

ts
const devRetention = {
  retention: {
    count: 3, // Keep only last 3 backups
  },
}

Manual cleanup:

ts
const retentionManager = new RetentionManager(
  { count: 5, maxAge: 30 },
  './backups'
)

const result = await retentionManager.cleanup()
console.log(`Deleted ${result.deletedCount} old backups`)

Released under the MIT License.