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, nilView on GitHub (pinned to 4ed7a308f6)
Solutions
- Confirm the WAL file exists and is at least WALHeaderSize (32) bytes
- Check litestream has read access to the database directory
- Fix the underlying read error surfaced by %w and let sync retry
- 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
- Ensure exactly one writer owns the database
- Keep DB files on reliable local storage
- Verify file sizes: WAL must be >= 32 bytes to be readable
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
- read wal header after expected truncation: %w
- detect full checkpoint: %w
- cannot read last synced wal page: %w
- set synchronous: %w
- checkpoint: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/f4ce1e5d6c2aadb4.
Report an issue: GitHub.