benbjohnson/litestream · error
snapshot wal read exceeded bound: max offset %d > end offset
Error message
snapshot wal read exceeded bound: max offset %d > end offset %d
What it means
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.
Source
Thrown at db.go:2848
// Build a mapping of changed page numbers and their latest content.
maxBytes := pos.walEndOffset - WALHeaderSize
pageMap := make(map[uint32]int64)
var maxOffset int64
var walCommit uint32
if maxBytes > 0 {
pageMap, maxOffset, walCommit, _, err = rd.pageMap(ctx, maxBytes)
if err != nil {
pw.CloseWithError(fmt.Errorf("page map: %w", err))
return
}
}
if walCommit > 0 {
commit = walCommit
}
if maxOffset > pos.walEndOffset {
pw.CloseWithError(fmt.Errorf("snapshot wal read exceeded bound: max offset %d > end offset %d", maxOffset, pos.walEndOffset))
return
}
walOffset, walSize := snapshotHeaderWALRange(maxOffset, int64(WALFrameHeaderSize+pos.pageSize))
db.Logger.Debug("encode snapshot header",
"txid", pos.pos.TXID.String(),
"commit", commit,
"walOffset", walOffset,
"walSize", walSize,
"walEndOffset", pos.walEndOffset,
"salt1", rd.salt1,
"salt2", rd.salt2)
enc, err := ltx.NewEncoder(pw)
if err != nil {
pw.CloseWithError(fmt.Errorf("new ltx encoder: %w", err))
return
}View on GitHub (pinned to 4ed7a308f6)
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
Defensive patterns
Strategy: retry
Validate before calling
// detect multiple replicators before snapshotting
if anotherLitestreamRunning(pidFile) { return errors.New("litestream already running for this db") } Try / catch
rc, err := db.SnapshotReader(ctx, pos)
if err != nil && strings.Contains(err.Error(), "exceeded bound") {
// WAL raced the snapshot; refresh pos and retry once
pos, _ = db.SnapshotPos(ctx)
rc, err = db.SnapshotReader(ctx, pos)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- set synchronous: %w
- checkpoint: %w
- checkpoint failed: %w
- sync database %s: %w
- enable wal failed, mode=%q
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/70c03d4052886dfb.
Report an issue: GitHub.