benbjohnson/litestream · error

read wal header after expected truncation: %w

Error message

read wal header after expected truncation: %w

What it means

After an expected WAL truncation (normal checkpoint behavior, issue #927), verify reads the new WAL header to learn the current salt values. If readWALHeader fails here, the expected-truncation fast path cannot be taken and the sync errors instead of continuing incrementally.

Source

Thrown at db.go:1723

	info.offset = dec.Header().WALOffset + dec.Header().WALSize
	info.salt1 = dec.Header().WALSalt1
	info.salt2 = dec.Header().WALSalt2
	info.prevCommit = dec.Header().Commit

	// If LTX WAL offset is larger than real WAL then the WAL has been truncated.
	if fi, err := os.Stat(db.WALPath()); err != nil {
		return info, fmt.Errorf("open wal file: %w", err)
	} else if info.offset > fi.Size() {
		exec.state.truncatePassiveFailed = false

		// If we previously synced to the exact end of the WAL, this truncation
		// is expected (normal checkpoint behavior). Reset position and continue
		// incrementally rather than triggering a full snapshot. See issue #927.
		if exec.state.syncedToWALEnd {
			// Read new WAL header to get current salt values
			hdr, err := readWALHeader(db.WALPath())
			if err != nil {
				return info, fmt.Errorf("read wal header after expected truncation: %w", err)
			}

			info.offset = WALHeaderSize
			info.salt1 = binary.BigEndian.Uint32(hdr[16:])
			info.salt2 = binary.BigEndian.Uint32(hdr[20:])
			info.snapshotting = false
			info.reason = ""
			info.clearSyncedToWALEnd = true

			db.Logger.Log(ctx, internal.LevelTrace, "wal truncated after sync to end (expected checkpoint)",
				"new_salt1", info.salt1,
				"new_salt2", info.salt2)

			return info, nil
		}

		info.reason = "wal truncated by another process"
		return info, nil

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Verify the -wal file exists and is readable at db.WALPath()
  2. Check for external scripts/jobs touching the WAL during checkpoints
  3. Fix the wrapped read error (permissions, mount health) and allow the next sync to retry
  4. If local state is desynced, `litestream reset` and resnapshot
Defensive patterns

Strategy: retry

Validate before calling

hdr, err := readWALHeader(dbPath + "-wal")
if err != nil || len(hdr) < 32 { /* WAL unreadable or too short */ }

Try / catch

if err := db.Sync(ctx); err != nil {
    if strings.Contains(err.Error(), "read wal header") {
        // transient: retry with backoff; persistent: reset state
    }
}

Prevention

When it happens

Trigger: syncedToWALEnd was true, info.offset exceeded the (truncated) WAL size, and readWALHeader(db.WALPath()) fails — WAL removed or unreadable right after a checkpoint truncated it.

Common situations: Checkpoint raced with file removal by an external tool; permissions changed mid-run; transient IO errors on network filesystems.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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