benbjohnson/litestream · critical · LTXError
decode
Error message
decode
What it means
An LTX file's header could not be decoded, which litestream treats as definitive corruption of that LTX file. It is wrapped in an *LTXError tagged "decode" and joined with ErrLTXCorrupted so callers (and auto-recover logic) can detect corruption specifically.
Source
Thrown at db.go:1702
info.snapshotting = true
if exec.pos.TXID == 0 {
info.offset = WALHeaderSize
return info, nil // first sync
}
// Determine last WAL offset we save from.
ltxPath := db.LTXPath(0, exec.pos.TXID, exec.pos.TXID)
ltxFile, err := os.Open(ltxPath)
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 valuesView on GitHub (pinned to 4ed7a308f6)
Solutions
- Run `litestream reset <db>` to clear corrupted local LTX state and resnapshot
- Restore from the replica if the remote copy is intact (auto-recover: true automates this on repeated LTX errors)
- Check for ENOSPC/disk-full events around the failure time and free space
- Verify storage integrity of the LTX directory (fsck, checksums)
Example fix
// config before
replicas:
- url: s3://bucket/db
// config after (auto-reset local corruption)
replicas:
- url: s3://bucket/db
auto-recover: true Defensive patterns
Strategy: fallback
Validate before calling
// before relying on local state, check LTX integrity
f, err := os.Open(ltxPath); if err == nil {
dec := ltx.NewDecoder(f)
err = dec.DecodeHeader() // corrupt if err != nil
} Type guard
var ltxErr *litestream.LTXError isCorrupt := errors.As(err, <xErr) && errors.Is(err, litestream.ErrLTXCorrupted)
Try / catch
if err := db.Sync(ctx); err != nil {
var ltxErr *litestream.LTXError
if errors.As(err, <xErr) && errors.Is(err, litestream.ErrLTXCorrupted) {
// reset local state and resnapshot from replica
litestream.Reset(ctx, dbPath)
}
} Prevention
- Enable auto-recover: true on the replica for automatic reset on LTX errors
- Monitor for ENOSPC; keep free disk headroom for WAL growth
- Never manually truncate or edit LTX files; use `litestream reset`
When it happens
Trigger: dec.DecodeHeader() fails while reading the LTX file for the current position during verifyWithExecutor — truncated/partial LTX file, non-LTX bytes at the expected path, or bit rot on disk.
Common situations: Process killed mid-write leaving a partial LTX file; disk full during replication (truncated file); filesystem corruption; manually copying/moving LTX files incorrectly.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- decode ltx page: %w
- decode header: %w
- decode page: %w
- invalid replica, checksum mismatch
- ltx file corrupted
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/70b87d61e400a382.
Report an issue: GitHub.