benbjohnson/litestream · error

enforce snapshot retention: %w

Error message

enforce snapshot retention: %w

What it means

Store.EnforceSnapshotRetention failed while db.EnforceSnapshotRetention enumerated and pruned old snapshot-level LTX files on the replica. The method lists all snapshot LTX files via the replica client and deletes those older than the retention window; failures occur during listing or deletion. The underlying storage error is wrapped with %w.

Source

Thrown at store.go:839

	db.Logger.InfoContext(ctx, "compaction complete",
		"level", dstLevel,
		slog.Group("txid",
			"min", info.MinTXID.String(),
			"max", info.MaxTXID.String(),
		),
		"size", info.Size,
	)

	return info, nil
}

// EnforceSnapshotRetention removes old snapshots by timestamp and then
// cleans up all lower levels based on minimum snapshot TXID.
func (s *Store) EnforceSnapshotRetention(ctx context.Context, db *DB) error {
	// Enforce retention for the snapshot level.
	minSnapshotTXID, err := db.EnforceSnapshotRetention(ctx, time.Now().Add(-s.SnapshotRetention))
	if err != nil {
		return fmt.Errorf("enforce snapshot retention: %w", err)
	}

	// We should also enforce retention for L0 on the same schedule as L1.
	for _, lvl := range s.levels {
		// Skip L0 since it is enforced on a more frequent basis.
		if lvl.Level == 0 {
			continue
		}

		if err := db.EnforceRetentionByTXID(ctx, lvl.Level, minSnapshotTXID); err != nil {
			return fmt.Errorf("enforce L%d retention: %w", lvl.Level, err)
		}
	}

	return nil
}

// ValidationResult holds the result of validating a replica's LTX files.

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Grant the replica credentials delete permissions on the bucket/prefix; check for object-lock or lifecycle policies that forbid deletion.
  2. Check the wrapped error (fetch vs delete phase) and fix the corresponding storage configuration.
  3. Verify SnapshotRetention is set to a sane value; an extremely negative/clock-skewed timestamp can produce unexpected prune sets.
  4. Retry after transient network errors; retention enforcement is idempotent.
  5. Disable retention only if a cloud lifecycle policy handles cleanup (RetentionEnabled=false).

Example fix

// before
{"storage": {"type": "s3", "bucket": "backups", "access-key-id": "..."}}
// after
{"storage": {"type": "s3", "bucket": "backups", "access-key-id": "...", 
  "comment": "IAM policy must allow s3:DeleteObject for retention pruning"}}
Defensive patterns

Strategy: validation

Validate before calling

// preflight: verify delete permission on snapshot prefix
itr, err := client.LTXFiles(ctx, litestream.SnapshotLevel, 0, false)
if err != nil {
    return fmt.Errorf("cannot list snapshots: %w", err)
}
itr.Close()

Try / catch

if err := store.EnforceSnapshotRetention(ctx, db); err != nil {
    if isPermissionError(err) {
        log.Printf("retention blocked by storage permissions: %v", err)
        // fix IAM policy, then retry
    }
    return err
}

Prevention

When it happens

Trigger: EnforceSnapshotRetention(ctx, db) (called from runOnce and monitorCompactionLevel) where Client.LTXFiles(ctx, SnapshotLevel, ...) fails or a delete operation on the storage backend fails.

Common situations: Storage credentials lacking delete permission (s3:DeleteObject), object-lock/immutability policies blocking deletes, network failure mid-pruning, or bucket misconfiguration.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/cd0957b100438755. Report an issue: GitHub.