{"record":{"id":"09779e81a2df383a","repo":"benbjohnson/litestream","slug":"w-expected-txid-d-but-remote-has-d","errorCode":null,"errorMessage":"%w: expected TXID %d but remote has %d","messagePattern":"%w: expected TXID (.+?) but remote has (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"vfs.go","lineNumber":2088,"sourceCode":"\tdefer itr.Close()\n\n\tvar remoteTXID ltx.TXID\n\tfor itr.Next() {\n\t\tinfo := itr.Item()\n\t\tif info.MaxTXID > remoteTXID {\n\t\t\tremoteTXID = info.MaxTXID\n\t\t}\n\t}\n\tif err := itr.Close(); err != nil {\n\t\treturn fmt.Errorf(\"iterate remote files: %w\", err)\n\t}\n\n\t// If remote has advanced beyond our expected position, we have a conflict\n\tif remoteTXID > f.expectedTXID {\n\t\tf.logger.Warn(\"conflict detected\",\n\t\t\t\"expected\", f.expectedTXID,\n\t\t\t\"remote\", remoteTXID)\n\t\treturn fmt.Errorf(\"%w: expected TXID %d but remote has %d\",\n\t\t\tErrConflict, f.expectedTXID, remoteTXID)\n\t}\n\n\treturn nil\n}\n\n// createLTXFromDirty creates an LTX file from dirty pages.\n// Returns a streaming reader for the LTX data using io.Pipe to avoid loading\n// all data into memory at once.\n// Must be called with f.mu held.\nfunc (f *VFSFile) createLTXFromDirty() io.Reader {\n\tpr, pw := io.Pipe()\n\n\t// Sort page numbers (LTX encoder requires ordered pages)\n\tpgnos := make([]uint32, 0, len(f.dirty))\n\tfor pgno := range f.dirty {\n\t\tpgnos = append(pgnos, pgno)\n\t}","sourceCodeStart":2070,"sourceCodeEnd":2106,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/vfs.go#L2070-L2106","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: two hosts, both running replicate with same config\n// after: ensure single active writer\nif errors.Is(err, ErrConflict) {\n    // remote is ahead: reset local cache and re-sync\n    if rerr := store.Reset(ctx, dbPath); rerr != nil { log.Fatal(rerr) }\n}","handlingStrategy":"validation","validationCode":"// before sync, compare local expected TXID with remote\nitr, _ := client.LTXFiles(ctx, 0, expectedTXID, false)\nvar remote ltx.TXID\nfor itr.Next() { if it := itr.Item(); it.MaxTXID > remote { remote = it.MaxTXID } }\nitr.Close()\nif remote > expectedTXID { return fmt.Errorf(\"remote ahead: %d > %d\", remote, expectedTXID) }","typeGuard":null,"tryCatchPattern":"err := db.Sync(ctx)\nif errors.Is(err, ErrConflict) {\n    // remote ahead: single-writer violated; reset local state and re-sync\n    return store.Reset(ctx, dbPath)\n}","preventionTips":["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"],"tags":["go","conflict","replication","distributed"],"backgroundTag":"invalid-state-transition","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}