benbjohnson/litestream · critical
non-contiguous ltx file: level=%d, current=%s, next=%s-%s
Error message
non-contiguous ltx file: level=%d, current=%s, next=%s-%s
What it means
While polling a level, an LTX file whose MinTXID does not continue the sequence (info.MinTXID != maxTXID+1) was found. The index would become inconsistent if applied, so the poll aborts with this error. L0 gaps are tolerated (deferred), other levels are hard failures.
Source
Thrown at vfs.go:2743
index := make(map[uint32]ltx.PageIndexElem)
maxTXID := prevMaxTXID
lastCommit := baseCommit
newCommit := baseCommit
replaceIndex := false
for itr.Next() {
info := itr.Item()
f.mu.Lock()
isNextTXID := info.MinTXID == maxTXID+1
f.mu.Unlock()
if !isNextTXID {
if level == 0 && info.MinTXID > maxTXID+1 {
f.logger.Warn("ltx gap detected at L0, deferring to higher levels", "expected", maxTXID+1, "next", info.MinTXID)
break
}
return maxTXID, nil, newCommit, replaceIndex, fmt.Errorf("non-contiguous ltx file: level=%d, current=%s, next=%s-%s", level, maxTXID, info.MinTXID, info.MaxTXID)
}
f.logger.Debug("new ltx file", "level", info.Level, "min", info.MinTXID, "max", info.MaxTXID)
idx, err := FetchPageIndex(ctx, f.client, info)
if err != nil {
return maxTXID, nil, newCommit, replaceIndex, fmt.Errorf("fetch page index: %w", err)
}
hdr, err := FetchLTXHeader(ctx, f.client, info)
if err != nil {
return maxTXID, nil, newCommit, replaceIndex, fmt.Errorf("fetch header: %w", err)
}
if hdr.Commit < lastCommit {
replaceIndex = true
index = make(map[uint32]ltx.PageIndexElem)
}
lastCommit = hdr.CommitView on GitHub (pinned to 4ed7a308f6)
Solutions
- Compare listed LTX files in storage against expected TXID sequence to find the gap
- Run `litestream reset` (or the replica auto-recover option) to rebuild local state from the replica
- Ensure only one litestream process replicates each database path
- Disable object-store lifecycle policies that delete LTX files under retention
Defensive patterns
Strategy: try-catch
Validate before calling
// verify contiguity before relying on replica reads
infos := listLTXInfos(level)
for i := 1; i < len(infos); i++ {
if infos[i].MinTXID != infos[i-1].MaxTXID+1 {
return fmt.Errorf("gap at %s", infos[i].MinTXID)
}
} Try / catch
if err := pollOnce(ctx); err != nil {
var gapErr *ContiguityError // or strings.Contains "non-contiguous ltx file"
if strings.Contains(err.Error(), "non-contiguous ltx file") {
return resetAndRehydrate(dbPath) // litestream reset
}
return err
} Prevention
- Never manually delete LTX files from replica storage
- Ensure a single writer per database (use leasing)
- Review retention/compaction settings before enabling lifecycle policies
When it happens
Trigger: Replica storage at level > 0 has missing LTX files: files deleted out-of-band, partial snapshot restore, concurrent compaction race, or a storage lifecycle rule removing files.
Common situations: Manually pruning object storage; restoring a subset of LTX files; two litestream processes replicating to the same path; failed compaction leaving holes.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- sync interval must be greater than 0
- lease not held
- disk full
- remote has newer transactions than expected
- validation failed
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/18d5b34bcddbb0dc.
Report an issue: GitHub.