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
- Free disk space or raise the quota on the data volume.
- Check whether other processes (logs, compaction temps) are consuming space concurrently and throttle them.
- Verify the LTX path has enough headroom for the max snapshot size before starting a snapshot.
- 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
- Reserve headroom above peak DB+WAL size on the data volume.
- Throttle co-located write-heavy workloads.
- Alert on disk usage trends, not just 100% full.
- Prefer volumes without tight per-user quotas for the data dir.
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.