benbjohnson/litestream · error
open wal file: %w
Error message
open wal file: %w
What it means
os.Stat on the database's live WAL file failed while verify was checking whether the LTX WAL offset exceeds the real WAL size. The sync cannot compare LTX state against the WAL if the WAL file cannot be stat'ed.
Source
Thrown at db.go:1712
if err != nil {
return info, NewLTXError("open", ltxPath, 0, uint64(exec.pos.TXID), uint64(exec.pos.TXID), err)
}
defer func() { _ = ltxFile.Close() }()
dec := ltx.NewDecoder(ltxFile)
if err := dec.DecodeHeader(); err != nil {
// Decode failure indicates corruption
ltxErr := NewLTXError("decode", ltxPath, 0, uint64(exec.pos.TXID), uint64(exec.pos.TXID), fmt.Errorf("%w: %w", ErrLTXCorrupted, err))
return info, ltxErr
}
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 = ""View on GitHub (pinned to 4ed7a308f6)
Solutions
- Confirm the -wal file exists next to the database and the path in config matches the actual DB path
- Check filesystem permissions for the litestream user
- Fix the wrapped inner error (path, permissions, mount) and let the next sync retry
- If the database was replaced, restart litestream and reset local state
Defensive patterns
Strategy: validation
Validate before calling
fi, err := os.Stat(dbPath + "-wal")
if err != nil { /* refuse to sync: WAL missing or unreadable */ } Type guard
func walExists(dbPath string) bool {
_, err := os.Stat(dbPath + "-wal")
return err == nil
} Try / catch
if err := db.Sync(ctx); err != nil {
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
// fix path/permissions, then retry sync
}
} Prevention
- Verify the config path points at the live database (with -wal present)
- Run litestream under a user with read access to the DB directory
- Avoid external jobs that move/delete WAL files
When it happens
Trigger: During verifyWithExecutor, os.Stat(db.WALPath()) returns an error — WAL file deleted between checkpoint and verify, or permission/IO error on the path.
Common situations: Another process deleted or moved the -wal file; database replaced on disk while litestream holds the old path; directory permissions changed; NFS/network filesystem glitches.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- get initial size: %w
- get size after delete: %w
- get final size: %w
- snapshot wal read exceeded bound: max offset %d > end offset
- list WAL segments: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/72a7f0679ab6119b.
Report an issue: GitHub.