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
- Stop all but one writer: ensure only a single litestream process/node replicates this database (use the S3 lease mechanism and verify lease config).
- Compare expected vs remote TXID in the message; if the remote is correct, reset local state (litestream reset) and re-sync from the replica.
- 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.
- 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
- Guarantee single writer: one litestream process per database, lease enabled
- After failover/failback, reset local state instead of reopening stale snapshots
- Alert on ErrConflict — it always means two writers or stale local state
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
- sync interval must be greater than 0
- lease not held
- remote has newer transactions than expected
- sync database: %w
- sync before disable: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/09779e81a2df383a.
Report an issue: GitHub.