benbjohnson/litestream · critical · LTXError

stage-close

stage-close

Error message

%w: %w (ErrDiskFull)

What it means

After fsync succeeded, litestream closes the LTX file; a close error classified as disk-full is reported as ErrDiskFull with stage 'stage-close'. On many filesystems close() performs the final flush of buffered data, so a full disk can surface here even though Sync() already returned success.

Source

Thrown at db.go:2222

			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
		s.walSize = sz
	})
	if err := ltxFile.Sync(); err != nil {
		if isDiskFullError(err) {
			return result, NewLTXError("stage-sync", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
		}
		return result, fmt.Errorf("sync ltx file: %w", err)
	}
	if err := ltxFile.Close(); err != nil {
		if isDiskFullError(err) {
			return result, NewLTXError("stage-close", tmpFilename, 0, uint64(txID), uint64(txID), fmt.Errorf("%w: %w", ErrDiskFull, err))
		}
		return result, fmt.Errorf("close ltx file: %w", err)
	}

	// Atomically rename file to final path.
	db.setSyncDiagPhase(diagPhaseRenameLTX, func(s *diagState) {
		s.txID = txID
		s.walSize = sz
	})
	if err := os.Rename(tmpFilename, filename); err != nil {
		db.maxLTXFileInfos.Lock()
		delete(db.maxLTXFileInfos.m, 0) // clear cache if in unknown state
		db.maxLTXFileInfos.Unlock()
		db.invalidatePosCache()
		return result, fmt.Errorf("rename ltx file: %w", err)
	}
	if err := internal.FsyncDir(filepath.Dir(filename)); err != nil {
		db.maxLTXFileInfos.Lock()

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Free disk space or raise the quota on the data volume.
  2. Check whether other processes (logs, compaction temps) are consuming space concurrently and throttle them.
  3. Verify the LTX path has enough headroom for the max snapshot size before starting a snapshot.
  4. Retry the sync after cleanup; the failed temp file is discarded.

Example fix

// before
// ignoring disk state before snapshots
res, err := db.Sync(ctx)
// after
if freeBytes(path) < minSnapshotHeadroom {
	return ErrDiskFull // pre-flight check
}
res, err := db.Sync(ctx)
if errors.Is(err, ErrDiskFull) { freeSpaceOrAlert() }
Defensive patterns

Strategy: try-catch

Validate before calling

if freeBytes(ltxDir) < maxSnapshotSize { return errors.New("insufficient headroom") }

Type guard

func isDiskFull(err error) bool { return errors.Is(err, ErrDiskFull) }

Try / catch

if err := db.Sync(ctx); err != nil {
	if errors.Is(err, ErrDiskFull) {
		// handle close-time ENOSPC: free space, retry
	}
	return err
}

Prevention

When it happens

Trigger: ltxFile.Close() at db.go:2220 returned ENOSPC/EDQUOT or a message matched by isDiskFullError. Typical when the disk filled between the fsync and the close, or on filesystems where close flushes remaining metadata/buffers.

Common situations: Rapid concurrent writes filling the volume in the milliseconds between sync and close; quota enforcement on close-time flush; small container overlay filesystems hitting capacity.

Related errors


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