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 values

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Run `litestream reset <db>` to clear corrupted local LTX state and resnapshot
  2. Restore from the replica if the remote copy is intact (auto-recover: true automates this on repeated LTX errors)
  3. Check for ENOSPC/disk-full events around the failure time and free space
  4. 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, &ltxErr) && errors.Is(err, litestream.ErrLTXCorrupted)

Try / catch

if err := db.Sync(ctx); err != nil {
    var ltxErr *litestream.LTXError
    if errors.As(err, &ltxErr) && errors.Is(err, litestream.ErrLTXCorrupted) {
        // reset local state and resnapshot from replica
        litestream.Reset(ctx, dbPath)
    }
}

Prevention

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


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/70b87d61e400a382. Report an issue: GitHub.