benbjohnson/litestream · error

cannot copy wal after passive checkpoint: %w

Error message

cannot copy wal after passive checkpoint: %w

What it means

After a PASSIVE checkpoint, Litestream must copy any WAL frames that accumulated since the checkpoint back into its replication stream via verifyAndSyncWithExecutor. If that sync fails, the checkpoint is reported as failed with this error. It means WAL frames written after the checkpoint were not captured and replication state is now inconsistent until a successful sync.

Source

Thrown at db.go:2561

	// If WAL hasn't been restarted, exit.
	db.setSyncDiagPhase(diagPhaseCheckpointVerifyRestart,
		func(s *diagState) {
			s.checkpointMode = mode
			s.lastSyncedWALOffset = exec.state.lastSyncedWALOffset
		})
	other, err := readWALHeader(db.WALPath())
	if err != nil {
		return false, err
	} else if bytes.Equal(hdr, other) {
		exec.state.syncedSinceCheckpoint = false
		return false, nil
	}
	exec.state.truncatePassiveFailed = false

	if mode == CheckpointModePassive {
		result, err = db.verifyAndSyncWithExecutor(ctx, true, exec, 0)
		if err != nil {
			return false, fmt.Errorf("cannot copy wal after passive checkpoint: %w", err)
		}
		exec.applySyncResult(result)
		exec.state.syncedSinceCheckpoint = false
		return true, nil
	}

	// A successful TRUNCATE checkpoint always reports zero frames because
	// the WAL is reset before the counters are read, so the comparison
	// below can never prove that no commits landed between the sealed
	// sync and the checkpoint taking the writer lock. Those commits are
	// backfilled and truncated unseen, so TRUNCATE must take the boundary
	// snapshot unconditionally.
	if mode != CheckpointModeTruncate && walFrameN <= preCheckpointFrameN {
		result, err = db.verifyAndSyncWithExecutor(ctx, true, exec, 0)
		if err != nil {
			return false, fmt.Errorf("cannot copy wal after checkpoint: %w", err)
		}
		exec.applySyncResult(result)

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Look at the wrapped error to determine whether the failure is on the WAL read side or the replica storage write side.
  2. Verify replica storage connectivity/credentials (e.g. S3 bucket reachable) and retry the sync.
  3. If the WAL appears corrupt or state is inconsistent, use 'litestream reset' for the database to rebuild local LTX state.
  4. Enable 'auto-recover' on the replica if you want Litestream to reset local state automatically on LTX errors.
Defensive patterns

Strategy: retry

Validate before calling

if err := replicaClientHealthCheck(ctx); err != nil { /* verify storage backend reachable before syncing */ }

Try / catch

if err != nil && strings.Contains(err.Error(), "cannot copy wal after passive checkpoint") {
    log.Printf("post-checkpoint WAL copy failed: %v", err)
    // backoff and retry; consider litestream reset if persistent
}

Prevention

When it happens

Trigger: mode == CheckpointModePassive and db.verifyAndSyncWithExecutor(ctx, true, exec, 0) returns an error immediately after the passive checkpoint: WAL read failure, checksum mismatch in the WAL, LTX write failure to the replica storage, or SQLITE_BUSY reading the db.

Common situations: Replica storage backend (S3, file, etc.) temporarily unavailable or returning errors during the post-checkpoint copy; corrupt or externally truncated WAL file; network interruption to cloud storage mid-sync.

Related errors


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