benbjohnson/litestream · error

truncate: %w

Error message

truncate: %w

What it means

applyLTXFile truncates the database file to hdr.Commit * pageSize bytes to match the committed size from the LTX transaction header. Failure here means the OS rejected the truncate (f.Truncate), leaving the local file at the wrong size, so the transaction cannot be considered applied.

Source

Thrown at replica.go:991

		if phdr.Pgno == 1 && len(data) >= 28 {
			data[18], data[19] = 0x01, 0x01
			_, _ = rand.Read(data[24:28])
		}

		off := int64(phdr.Pgno-1) * int64(pageSize)
		if _, err := f.WriteAt(data, off); err != nil {
			return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
		}
	}

	if hdr.Commit > 0 {
		if err := f.Sync(); err != nil {
			return fmt.Errorf("sync before truncate: %w", err)
		}
		newSize := int64(hdr.Commit) * int64(pageSize)
		if err := f.Truncate(newSize); err != nil {
			return fmt.Errorf("truncate: %w", err)
		}
	}

	if err := dec.Close(); err != nil {
		return fmt.Errorf("close decoder: %w", err)
	}

	return f.Sync()
}

// fillFollowGap attempts to bridge a gap in level 0 files by searching
// higher compaction levels for a file that covers the missing TXID range.
func (r *Replica) fillFollowGap(ctx context.Context, f *os.File, afterTXID ltx.TXID, gapMinTXID ltx.TXID, pageSize uint32) (ltx.TXID, error) {
	currentTXID := afterTXID

	for level := 1; level < SnapshotLevel; level++ {
		itr, err := r.Client.LTXFiles(ctx, level, 0, false)
		if err != nil {

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure the database file and its directory are writable by the litestream user (chown/chmod)
  2. Verify the volume is mounted read-write and has room for the target size
  3. Validate the LTX file header (hdr.Commit) is sane; reset corrupted state with `litestream reset <db>`
  4. Re-run restore/replication after fixing permissions or space

Example fix

# before
$ ls -l db.sqlite
-r--r--r-- 1 root root ... db.sqlite
# after
$ sudo chmod u+w db.sqlite && sudo chown litestream:litestream db.sqlite
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(dbPath)
if err != nil { return err }
if fi.Mode().Perm()&0200 == 0 {
    return fmt.Errorf("db file %s is not writable", dbPath)
}
if fi.Size() > int64(hdr.Commit)*int64(pageSize) {
    // shrink truncate expected; ensure space/perm, otherwise file may be locked read-only
    return fmt.Errorf("truncate target %d < current %d; check file state", int64(hdr.Commit)*int64(pageSize), fi.Size())
}

Try / catch

if err := r.applyLTXFile(ctx, f, info, pageSize); err != nil {
    if errors.Is(err, syscall.EACCES) || errors.Is(err, syscall.EROFS) {
        return fmt.Errorf("cannot truncate %s: fix permissions/mount: %w", dbPath, err)
    }
    return err
}

Prevention

When it happens

Trigger: f.Truncate(newSize) fails: read-only filesystem, permission denied on the file, newSize beyond filesystem/device limits, or an invalid hdr.Commit/pageSize combination producing an out-of-range size.

Common situations: Database file made read-only after a crash, running litestream as a user without write access, tiny tmpfs/disk where the committed size exceeds available space, corrupted LTX header yielding a nonsensical commit size.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


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