benbjohnson/litestream · error

cannot read wal header: %w

Error message

cannot read wal header: %w

What it means

verify compares the live WAL header salts against the salts recorded in the last LTX file to decide whether the WAL was restarted/rewritten. If the WAL header cannot be read at all, the comparison cannot happen and the sync fails with this error.

Source

Thrown at db.go:1747

			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
	}

	// Compare WAL headers. Restart from beginning of WAL if different.
	hdr0, err := readWALHeader(db.WALPath())
	if err != nil {
		return info, fmt.Errorf("cannot read wal header: %w", err)
	}
	salt1 := binary.BigEndian.Uint32(hdr0[16:])
	salt2 := binary.BigEndian.Uint32(hdr0[20:])
	saltMatch := salt1 == dec.Header().WALSalt1 && salt2 == dec.Header().WALSalt2
	if !saltMatch {
		exec.state.truncatePassiveFailed = false
	}

	// Handle edge case where we're at WAL header (WALOffset=32, WALSize=0).
	// This can happen when an LTX file represents a state at the beginning of the WAL
	// with no frames written yet. We must check this before computing prevWALOffset
	// to avoid underflow (32 - 4120 = -4088).
	// See: https://github.com/benbjohnson/litestream/issues/900
	if info.offset == WALHeaderSize {
		db.Logger.Debug("verify", "saltMatch", saltMatch, "atWALHeader", true)
		if saltMatch {
			info.snapshotting = false
			return info, nil

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Confirm the WAL file exists and is at least WALHeaderSize (32) bytes
  2. Check litestream has read access to the database directory
  3. Fix the underlying read error surfaced by %w and let sync retry
  4. Ensure no external process (backup scripts, other SQLite tools) is deleting the WAL
Defensive patterns

Strategy: retry

Validate before calling

hdr, err := readWALHeader(dbPath + "-wal")
if err != nil { /* cannot compare salts; fix access first */ }

Try / catch

if err := db.Sync(ctx); err != nil {
    if strings.Contains(err.Error(), "cannot read wal header") {
        // check WAL existence/permissions, then backoff-retry
    }
}

Prevention

When it happens

Trigger: readWALHeader(db.WALPath()) returns an error during verify — WAL missing, unreadable, or shorter than the 32-byte WAL header.

Common situations: Database checkpointed with delete mode removing the WAL at the exact verify moment; wrong DB path in config; broken mount or permissions.

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/f4ce1e5d6c2aadb4. Report an issue: GitHub.