benbjohnson/litestream · error

write ltx from wal: %w

Error message

write ltx from wal: %w

What it means

db.writeLTXFromWAL failed with a non-disk-full error while copying WAL frames into the incremental LTX file. This is the incremental replication path: frames are read from the WAL file and encoded as page data; any read/encode failure other than ENOSPC aborts the sync for this transaction.

Source

Thrown at db.go:2193

		if err := db.writeLTXFromDB(ctx, enc, walFile, commit, pageMap); err != nil {
			if isDiskFullError(err) {
				return result, NewLTXError("stage-write", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
			}
			return result, fmt.Errorf("write ltx from db: %w", err)
		}
	} else {
		db.setSyncDiagPhase(diagPhaseWriteLTXFromWAL,
			func(s *diagState) {
				s.txID = txID
				s.walSize = sz
				s.snapshotting = false
				s.reason = info.reason
			})
		if err := db.writeLTXFromWAL(ctx, enc, walFile, info.prevCommit, commit, pageMap); err != nil {
			if isDiskFullError(err) {
				return result, NewLTXError("stage-write", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
			}
			return result, fmt.Errorf("write ltx from wal: %w", err)
		}
	}

	// Encode final trailer to the end of the LTX file.
	db.setSyncDiagPhase(diagPhaseCloseLTX, func(s *diagState) {
		s.txID = txID
		s.walSize = sz
	})
	if err := enc.Close(); err != nil {
		if isDiskFullError(err) {
			return result, NewLTXError("stage-write", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
		}
		return result, fmt.Errorf("close ltx encoder: %w", err)
	}

	// Sync & close LTX file.
	db.setSyncDiagPhase(diagPhaseFsyncLTX, func(s *diagState) {
		s.txID = txID

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Verify no other process manipulates the database's WAL file; let litestream be the only WAL-aware monitor.
  2. Check WAL integrity (sqlite3 integrity_check) and confirm salt mismatch logs; a reset will resnapshot.
  3. Run `litestream reset` for the database to clear local LTX state and force a fresh snapshot.
  4. Check dmesg for storage read errors; retry after transient I/O issues clear.
  5. Make sure the app and litestream use compatible SQLite versions/builds (modernc.org/sqlite per repo guidance).

Example fix

// before: app cron checkpoints/truncates WAL externally
*/5 * * * * sqlite3 /data/app.db 'PRAGMA wal_checkpoint(TRUNCATE);'
// after: remove the cron; rely on litestream-managed sync/checkpointing
# (deleted cron entry)
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure WAL exists and is not being rotated externally:
if _, err := os.Stat(dbPath + "-wal"); err != nil {
    return fmt.Errorf("wal missing before sync: %w", err)
}

Try / catch

if err != nil && strings.Contains(err.Error(), "write ltx from wal") {
    log.Printf("incremental sync failed: %v; salts may have mismatched — consider litestream reset to resnapshot", err)
}

Prevention

When it happens

Trigger: WAL file truncated, rotated, or replaced mid-read (salt/frame mismatch with rd.salt1/salt2 captured earlier); read I/O error on the WAL; context cancellation; ltx encoder write failure.

Common situations: Another process deleting/rotating the -wal file (e.g. running `sqlite3 db 'PRAGMA wal_checkpoint(TRUNCATE)'` or VACUUM concurrently via a different toolchain); mismatched sqlite library versions; crash-truncated WAL after power loss.

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


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