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

  1. Ensure a single litestream process replicates each database — enable/configure leasing (conditional writes) to prevent concurrent replicators
  2. Avoid external WAL manipulation (manual rotation, copy-while-running) during replication
  3. Retry the snapshot; a transient race usually succeeds once the WAL is stable
  4. 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

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


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