benbjohnson/litestream · error

rename to output path: %w

Error message

rename to output path: %w

What it means

Once the fully restored database is assembled in the temp file, RestoreV3 atomically renames it from tmpPath to opt.OutputPath via os.Rename and wraps failures as 'rename to output path: %w'. At this point the data is downloaded and replayed; only the final placement failed.

Source

Thrown at replica.go:1157

	}

	// Create temp file for restore.
	tmpPath := opt.OutputPath + ".tmp"
	defer func() { _ = os.Remove(tmpPath) }()

	// Download and decompress snapshot.
	if err := r.downloadSnapshotV3(ctx, client, snapshot.Generation, snapshot.Index, tmpPath); err != nil {
		return fmt.Errorf("download snapshot: %w", err)
	}

	// Apply WAL segments.
	if err := r.applyWALSegmentsV3(ctx, client, snapshot.Generation, snapshot.Index, segments, tmpPath); err != nil {
		return fmt.Errorf("apply WAL segments: %w", err)
	}

	// Rename to final path.
	if err := os.Rename(tmpPath, opt.OutputPath); err != nil {
		return fmt.Errorf("rename to output path: %w", err)
	}
	if err := internal.FsyncDir(filepath.Dir(opt.OutputPath)); err != nil {
		return fmt.Errorf("sync restore output dir: %w", err)
	}

	if opt.IntegrityCheck != IntegrityCheckNone {
		if err := checkIntegrity(ctx, opt.OutputPath, opt.IntegrityCheck); err != nil {
			if ctx.Err() == nil {
				_ = os.Remove(opt.OutputPath)
				_ = os.Remove(opt.OutputPath + "-shm")
				_ = os.Remove(opt.OutputPath + "-wal")
			}
			return fmt.Errorf("post-restore integrity check: %w", err)
		}
		r.Logger().Info("post-restore integrity check passed")
	}

	return nil

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure opt.OutputPath's directory is writable and no file appeared at that path during the restore; remove the blocker and retry
  2. Avoid symlinked or mount-pointed output paths that resolve to a different filesystem; restore directly onto the target mount
  3. Read the wrapped cause: EEXIST means something created the path mid-restore, EXDEV means cross-device rename, EACCES means permissions
  4. If EEXIST, the .tmp file still holds a complete restore (before defer cleanup); consider recovering from it after verifying integrity

Example fix

// before
opt := litestream.RestoreOptions{OutputPath: "/mnt/data/db.sqlite"} // /mnt/data is a symlink to another device
// after
real, err := filepath.EvalSymlinks("/mnt/data") // resolve to same filesystem as temp dir
if err != nil {
    return err
}
opt := litestream.RestoreOptions{OutputPath: filepath.Join(real, "db.sqlite")}
Defensive patterns

Strategy: validation

Validate before calling

dir := filepath.Dir(outputPath)
// resolve symlinks so temp file and destination end up on the same filesystem
dirReal, err := filepath.EvalSymlinks(dir)
if err != nil {
    return err
}
if _, err := os.Stat(outputPath); err == nil {
    return fmt.Errorf("%s appeared mid-restore; remove it first", outputPath)
}

Try / catch

if err := replica.Restore(ctx, opt); err != nil {
    if strings.Contains(err.Error(), "rename to output path") {
        if strings.Contains(errors.Unwrap(err).Error(), "file exists") {
            return fmt.Errorf("%s was created during restore; remove and retry", opt.OutputPath)
        }
        if strings.Contains(errors.Unwrap(err).Error(), "invalid argument") || strings.Contains(errors.Unwrap(err).Error(), "cross") {
            return fmt.Errorf("temp and output are on different filesystems; choose OutputPath on the same mount")
        }
    }
    return err
}

Prevention

When it happens

Trigger: Calling Replica.Restore when the final rename fails: the destination filesystem differs from the temp file's (cross-device rename is impossible if tmpPath and OutputPath resolve to different mounts), a file appeared at OutputPath during the restore, or permission problems on the destination directory.

Common situations: OutputPath is a symlink or bind mount pointing to another filesystem than the temp file location; another process created OutputPath during a long restore (race with the earlier exists-check); restore run inside a container where /tmp and the destination are separate mounts and the temp path resolves differently; destination directory made read-only mid-restore.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


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