benbjohnson/litestream · critical · LTXError
stage-sync
stage-sync
Error message
%w: %w (ErrDiskFull)
What it means
During a WAL-to-LTX sync, litestream fsyncs the temporary LTX file before renaming it into place. If the fsync fails and the OS error indicates the filesystem is out of space, the error is wrapped as ErrDiskFull with the structured LTX stage marker 'stage-sync'. The library classifies this specifically so callers can react to disk exhaustion rather than treating it as a generic I/O failure.
Source
Thrown at db.go:2216
db.setSyncDiagPhase(diagPhaseCloseLTX, func(s *diagState) {
s.txID = txID
s.walSize = sz
})
if err := enc.Close(); err != nil {
if isDiskFullError(err) {
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 stateView on GitHub (pinned to 4ed7a308f6)
Solutions
- Free disk space on the volume holding the database/LTX path (delete old snapshots, logs, or truncate the WAL via checkpointing).
- Check filesystem quota with df -h and quota commands; raise the quota or move the data directory to a larger volume.
- Enable the replica's auto-recover option or run `litestream reset` if local LTX state is inconsistent after the failure.
- Configure disk usage alerts so space is reclaimed before ENOSPC; consider a lifecycle policy for retained LTX files.
- Retry the sync after space is freed; the operation is idempotent since the temp file is discarded on error.
Example fix
// before
if err := db.Sync(ctx); err != nil {
log.Fatal(err) // opaque failure
}
// after
if err := db.Sync(ctx); err != nil {
if errors.Is(err, ErrDiskFull) {
freeSpaceOrAlert() // handle ENOSPC specifically
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
const free = require('check-disk-space')
// before heavy writes / periodic check
if (freeBytes(dbPath) < 2 * maxExpectedWALSize) {
alertOrCleanUp() // avoid ENOSPC mid-sync
} 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) {
// free space / alert / retry after cleanup
}
return err
} Prevention
- Monitor free disk space and alert well before capacity.
- Set cloud lifecycle policies or retention so LTX files don't accumulate.
- Size volumes for max WAL growth plus snapshot headroom.
- Watch for EDQUOT: check quotas, not just df, on shared hosts.
When it happens
Trigger: The DB sync loop wrote the LTX file but ltxFile.Sync() returned ENOSPC/EDQUOT (or a message matched by isDiskFullError) at db.go:2214-2216. Happens on the periodic sync after WAL frames are appended while the disk filled up mid-sync.
Common situations: Small disk volumes with heavy write load; disk quota (EDQUOT) on shared hosts or containers with a volume size limit; logs or WAL growth consuming the last free blocks; snapshotting a large database where the temp LTX file plus WAL exceed free space.
Related errors
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/cf13a0887b658372.
Report an issue: GitHub.