benbjohnson/litestream · critical

%w: expected TXID %d but remote has %d

Error message

%w: expected TXID %d but remote has %d

What it means

Litestream detected a replication conflict: the remote replica has transactions (remoteTXID) newer than the TXID this VFS file expected (f.expectedTXID). Since a database replicates to exactly one destination, another writer (another node/process holding a lease) has committed LTX files ahead of this one, so local divergent history must not be silently overwritten. This wraps ErrConflict, which callers can match with errors.Is.

Source

Thrown at vfs.go:2088

	defer itr.Close()

	var remoteTXID ltx.TXID
	for itr.Next() {
		info := itr.Item()
		if info.MaxTXID > remoteTXID {
			remoteTXID = info.MaxTXID
		}
	}
	if err := itr.Close(); err != nil {
		return fmt.Errorf("iterate remote files: %w", err)
	}

	// If remote has advanced beyond our expected position, we have a conflict
	if remoteTXID > f.expectedTXID {
		f.logger.Warn("conflict detected",
			"expected", f.expectedTXID,
			"remote", remoteTXID)
		return fmt.Errorf("%w: expected TXID %d but remote has %d",
			ErrConflict, f.expectedTXID, remoteTXID)
	}

	return nil
}

// createLTXFromDirty creates an LTX file from dirty pages.
// Returns a streaming reader for the LTX data using io.Pipe to avoid loading
// all data into memory at once.
// Must be called with f.mu held.
func (f *VFSFile) createLTXFromDirty() io.Reader {
	pr, pw := io.Pipe()

	// Sort page numbers (LTX encoder requires ordered pages)
	pgnos := make([]uint32, 0, len(f.dirty))
	for pgno := range f.dirty {
		pgnos = append(pgnos, pgno)
	}

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Stop all but one writer: ensure only a single litestream process/node replicates this database (use the S3 lease mechanism and verify lease config).
  2. Compare expected vs remote TXID in the message; if the remote is correct, reset local state (litestream reset) and re-sync from the replica.
  3. If the local database is authoritative, verify the remote history was not produced by a stale node before forcing anything; never hand-delete remote LTX files while another writer is active.
  4. After failback, let the leased primary resume replication rather than reopening an out-of-date snapshot against the live replica.

Example fix

// before: two hosts, both running replicate with same config
// after: ensure single active writer
if errors.Is(err, ErrConflict) {
    // remote is ahead: reset local cache and re-sync
    if rerr := store.Reset(ctx, dbPath); rerr != nil { log.Fatal(rerr) }
}
Defensive patterns

Strategy: validation

Validate before calling

// before sync, compare local expected TXID with remote
itr, _ := client.LTXFiles(ctx, 0, expectedTXID, false)
var remote ltx.TXID
for itr.Next() { if it := itr.Item(); it.MaxTXID > remote { remote = it.MaxTXID } }
itr.Close()
if remote > expectedTXID { return fmt.Errorf("remote ahead: %d > %d", remote, expectedTXID) }

Try / catch

err := db.Sync(ctx)
if errors.Is(err, ErrConflict) {
    // remote ahead: single-writer violated; reset local state and re-sync
    return store.Reset(ctx, dbPath)
}

Prevention

When it happens

Trigger: Sync's checkForConflict finds remoteTXID > f.expectedTXID: a second process/node replicated newer transactions while this one had cached an older expected position (e.g. after snapshot restore, failback, or two active writers without leasing).

Common situations: Running litestream on two nodes against the same database/replica (missing or expired distributed lease); failing back to a primary whose WAL was ahead; restoring an old snapshot and reopening against an advanced replica; manual litestream replicate invocations alongside the service.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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