benbjohnson/litestream · error

cannot specify both TXID & timestamp to restore

Error message

cannot specify both TXID & timestamp to restore

What it means

CalcRestorePlan rejects a restore request that supplies both a TXID and a timestamp, because the restore target is ambiguous. Exactly one of txID or timestamp must be nonzero/zero respectively. The public API surfaces this as a plain error with no wrapped cause.

Source

Thrown at replica.go:1516

	// Find snapshots at the snapshot level that are before the timestamp.
	snapshots, err := FindLTXFiles(ctx, r.Client, SnapshotLevel, true, func(info *ltx.FileInfo) (bool, error) {
		return info.CreatedAt.Before(timestamp), nil
	})
	if err != nil {
		return nil, fmt.Errorf("find LTX snapshots: %w", err)
	}
	if len(snapshots) == 0 {
		return nil, nil
	}

	// Return the latest snapshot before the timestamp (last in the sorted list).
	return snapshots[len(snapshots)-1], nil
}

// CalcRestorePlan returns a list of storage paths to restore a snapshot at the given TXID.
func CalcRestorePlan(ctx context.Context, client ReplicaClient, txID ltx.TXID, timestamp time.Time, logger *slog.Logger) ([]*ltx.FileInfo, error) {
	if txID != 0 && !timestamp.IsZero() {
		return nil, fmt.Errorf("cannot specify both TXID & timestamp to restore")
	}

	var infos ltx.FileInfoSlice
	logger = logger.With("target", txID)

	// Start with latest snapshot before target TXID or timestamp.
	// Pass useMetadata flag to enable accurate timestamp fetching for timestamp-based restore.
	var snapshot *ltx.FileInfo
	snapshotItr, err := client.LTXFiles(ctx, SnapshotLevel, 0, !timestamp.IsZero())
	if err != nil {
		return nil, err
	}
	for snapshotItr.Next() {
		info := snapshotItr.Item()
		logger.Debug("finding snapshot before target TXID or timestamp", "snapshot", info.MaxTXID)
		if txID != 0 && info.MaxTXID > txID {
			continue
		}

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Pass only one target: set timestamp to the zero value time.Time{} when restoring by TXID.
  2. If using the CLI, drop either -txid or -timestamp.
  3. Clear a stale persisted target time (SetTargetTime/ResetTime state) before issuing a TXID restore.

Example fix

// before
plan, err := CalcRestorePlan(ctx, client, txID, targetTime, logger)
// after
var zeroTime time.Time
plan, err := CalcRestorePlan(ctx, client, txID, zeroTime, logger)
Defensive patterns

Strategy: validation

Validate before calling

// Go: enforce mutual exclusivity before calling the API
if txID != 0 && !timestamp.IsZero() {
    return fmt.Errorf("pass either txID or timestamp, not both")
}
plan, err := ltx.CalcRestorePlan(ctx, client, txID, timestamp, logger)

Try / catch

if err != nil {
    if err.Error() == "cannot specify both TXID & timestamp to restore" {
        // clear one of the targets and rebuild the plan
    }
    return err
}

Prevention

When it happens

Trigger: Calling Restore/CalcRestorePlan (or triggering SetTargetTime/ResetTime/waitForRestorePlan paths) with both txID != 0 and timestamp set, e.g. via library API CalcRestorePlan(ctx, client, txid, ts, logger) or CLI flags combining -txid and -timestamp.

Common situations: Library users computing a plan for a known TXID while leaving a default timestamp from config; scripts building restore args that always append -timestamp; restoring to a specific transaction while a stale target time is persisted.

Related errors


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