benbjohnson/litestream · critical · LTXError

stage-open

stage-open

Error message

%w: %w (ErrDiskFull)

What it means

Opening the temporary LTX file (<filename>.tmp) for read-write create failed with a disk-full condition, wrapped in an LTXError with code stage-open and classified as ErrDiskFull. Creation can also consume metadata blocks even if the WAL data would fit.

Solutions

  1. Free disk space (or expand the volume) on the staging directory's filesystem
  2. Check inode usage (df -i) and quotas, not just bytes
  3. Move the Litestream staging directory to larger storage via config
  4. Retry the sync after space is reclaimed; no reset required

Example fix

// before
$ df -i /var/lib/litestream  # inodes exhausted
// after
$ find /var/lib/litestream/tmp -type f -delete  # or raise quota/inodes
$ systemctl restart litestream
Defensive patterns

Strategy: fallback

Validate before calling

var st syscall.Statfs_t
if err := syscall.Statfs(stageDir, &st); err == nil {
	if st.Bavail*uint64(st.Bsize) < minFreeBytes || st.Ffree < minFreeInodes {
		return errors.New("insufficient disk/inodes for staging")
	}
}

Try / catch

var ltxErr *litestream.LTXError
if errors.As(err, &ltxErr) && errors.Is(ltxErr, litestream.ErrDiskFull) && ltxErr.Code == "stage-open" {
	freeDiskOrExpandVolume()
	nextSyncWillRetry() // no reset needed
}

Prevention

When it happens

Trigger: db.openLTXFile(tmpFilename, O_RDWR|O_CREATE|O_TRUNC) returns ENOSPC (via isDiskFullError) while creating the temp file in the staging directory.

Common situations: Disk filled between the mkdir and open steps on a nearly-full volume; inode/quota exhaustion manifesting as ENOSPC; container storage limits hit during a large snapshot stage.

Understand the failure class

Background: "File too large" / "file size exceeds limit" errors: why libraries cap file sizes and how to fix them — this error's family across 46 libraries.

Related errors


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

Appendix: source

Thrown at db.go:2123

	// Exit if we have no new WAL pages and we aren't snapshotting.
	if !info.snapshotting && sz == 0 {
		db.Logger.Log(ctx, internal.LevelTrace, "sync: skip", "reason", "no new wal pages")
		return result, nil
	}

	tmpFilename := filename + ".tmp"
	if err := internal.MkdirAll(filepath.Dir(tmpFilename), db.dirInfo); err != nil {
		if isDiskFullError(err) {
			return result, NewLTXError("stage-mkdir", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
		}
		return result, err
	}

	ltxFile, err := db.openLTXFile(tmpFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
	if err != nil {
		if isDiskFullError(err) {
			return result, NewLTXError("stage-open", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
		}
		return result, fmt.Errorf("open temp ltx file: %w", err)
	}
	defer func() { _ = os.Remove(tmpFilename) }()
	defer func() { _ = ltxFile.Close() }()

	uid, gid := internal.Fileinfo(db.fileInfo)
	_ = os.Chown(tmpFilename, uid, gid)

	db.Logger.Log(ctx, internal.LevelTrace, "encode header",
		"txid", txID.String(),
		"commit", commit,
		"walOffset", info.offset,
		"walSize", sz,
		"salt1", rd.salt1,
		"salt2", rd.salt2)

	timestamp := time.Now()

View on GitHub (pinned to 4ed7a308f6)