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.

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)

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.