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, nilView on GitHub (pinned to 4ed7a308f6)
Solutions
- Verify the -wal file exists and is readable at db.WALPath()
- Check for external scripts/jobs touching the WAL during checkpoints
- Fix the wrapped read error (permissions, mount health) and allow the next sync to retry
- 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
- Don't run WAL-manipulating scripts while litestream monitors the DB
- Use a local filesystem, not NFS, for the monitored database
- Retry sync with exponential backoff for transient read errors
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
- checkpoint: %w
- checkpoint failed: %w
- cannot read wal header: %w
- detect full checkpoint: %w
- short read page %d @ %d
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/2d9386a1ede1a800.
Report an issue: GitHub.