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.Commit

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Compare listed LTX files in storage against expected TXID sequence to find the gap
  2. Run `litestream reset` (or the replica auto-recover option) to rebuild local state from the replica
  3. Ensure only one litestream process replicates each database path
  4. 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

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


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