{"record":{"id":"70c03d4052886dfb","repo":"benbjohnson/litestream","slug":"snapshot-wal-read-exceeded-bound-max-offset-d","errorCode":null,"errorMessage":"snapshot wal read exceeded bound: max offset %d > end offset %d","messagePattern":"snapshot wal read exceeded bound: max offset (.+?) > end offset (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"db.go","lineNumber":2848,"sourceCode":"\n\t\t// Build a mapping of changed page numbers and their latest content.\n\t\tmaxBytes := pos.walEndOffset - WALHeaderSize\n\t\tpageMap := make(map[uint32]int64)\n\t\tvar maxOffset int64\n\t\tvar walCommit uint32\n\t\tif maxBytes > 0 {\n\t\t\tpageMap, maxOffset, walCommit, _, err = rd.pageMap(ctx, maxBytes)\n\t\t\tif err != nil {\n\t\t\t\tpw.CloseWithError(fmt.Errorf(\"page map: %w\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif walCommit > 0 {\n\t\t\tcommit = walCommit\n\t\t}\n\n\t\tif maxOffset > pos.walEndOffset {\n\t\t\tpw.CloseWithError(fmt.Errorf(\"snapshot wal read exceeded bound: max offset %d > end offset %d\", maxOffset, pos.walEndOffset))\n\t\t\treturn\n\t\t}\n\t\twalOffset, walSize := snapshotHeaderWALRange(maxOffset, int64(WALFrameHeaderSize+pos.pageSize))\n\n\t\tdb.Logger.Debug(\"encode snapshot header\",\n\t\t\t\"txid\", pos.pos.TXID.String(),\n\t\t\t\"commit\", commit,\n\t\t\t\"walOffset\", walOffset,\n\t\t\t\"walSize\", walSize,\n\t\t\t\"walEndOffset\", pos.walEndOffset,\n\t\t\t\"salt1\", rd.salt1,\n\t\t\t\"salt2\", rd.salt2)\n\n\t\tenc, err := ltx.NewEncoder(pw)\n\t\tif err != nil {\n\t\t\tpw.CloseWithError(fmt.Errorf(\"new ltx encoder: %w\", err))\n\t\t\treturn\n\t\t}","sourceCodeStart":2830,"sourceCodeEnd":2866,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/db.go#L2830-L2866","documentation":"A consistency guard: after building the page map, litestream asserts the highest WAL offset actually read (maxOffset) does not exceed the end offset captured when the snapshot position was taken (pos.walEndOffset). If it does, the WAL changed between snapshotting the end offset and reading it — the snapshot would be incoherent, so the stream aborts. This is an internal invariant violation, not user error.","triggerScenarios":"The WAL file was truncated, replaced, or restarted (new salt) between snapshotWALEndOffset(pos) and the page-map read inside the snapshot goroutine; or two processes are replicating the same database concurrently without a lease.","commonSituations":"Running two litestream instances against one DB (no leasing / replica contention); SQLite checkpoint-restarting the WAL mid-snapshot; external scripts rotating or deleting the WAL during backup; a stale pos reused after the WAL wrapped.","solutions":["Ensure a single litestream process replicates each database — enable/configure leasing (conditional writes) to prevent concurrent replicators","Avoid external WAL manipulation (manual rotation, copy-while-running) during replication","Retry the snapshot; a transient race usually succeeds once the WAL is stable","If reproducible, report it — this indicates a race between end-offset capture and streaming that may be a bug"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// detect multiple replicators before snapshotting\nif anotherLitestreamRunning(pidFile) { return errors.New(\"litestream already running for this db\") }","typeGuard":null,"tryCatchPattern":"rc, err := db.SnapshotReader(ctx, pos)\nif err != nil && strings.Contains(err.Error(), \"exceeded bound\") {\n    // WAL raced the snapshot; refresh pos and retry once\n    pos, _ = db.SnapshotPos(ctx)\n    rc, err = db.SnapshotReader(ctx, pos)\n}","preventionTips":["Run exactly one litestream instance per database","Never rotate or truncate the WAL externally during replication","Use the leasing feature for failover safety","Treat repeated occurrences as a bug report, not a config issue"],"tags":["go","sqlite","wal","race-condition","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}