dgraph-io/badger · critical

Equality can happen only on base level: %d

Error message

Equality can happen only on base level: %d

What it means

During skl.Skiplist.Put, the CAS-based insertion retries at each level; if prev[i] == next[i] at level i, the key already exists at that level. Since duplicates can only occur at the base level (0), finding equality at a higher level means the list is corrupt, so Badger panics with this assertion.

Source

Thrown at skl/skl.go:329

				// We haven't computed prev, next for this level because height exceeds old listHeight.
				// For these levels, we expect the lists to be sparse, so we can just search from head.
				prev[i], next[i] = s.findSpliceForLevel(key, s.head, i)
				// Someone adds the exact same key before we are able to do so. This can only happen on
				// the base level. But we know we are not on the base level.
				y.AssertTrue(prev[i] != next[i])
			}
			nextOffset := s.arena.getNodeOffset(next[i])
			x.tower[i].Store(nextOffset)
			if prev[i].casNextOffset(i, nextOffset, s.arena.getNodeOffset(x)) {
				// Managed to insert x between prev[i] and next[i]. Go to the next level.
				break
			}
			// CAS failed. We need to recompute prev and next.
			// It is unlikely to be helpful to try to use a different level as we redo the search,
			// because it is unlikely that lots of nodes are inserted between prev[i] and next[i].
			prev[i], next[i] = s.findSpliceForLevel(key, prev[i], i)
			if prev[i] == next[i] {
				y.AssertTruef(i == 0, "Equality can happen only on base level: %d", i)
				prev[i].setValue(s.arena, v)
				return
			}
		}
	}
}

// Empty returns if the Skiplist is empty.
func (s *Skiplist) Empty() bool {
	return s.findLast() == nil
}

// findLast returns the last element. If head (empty list), we return nil. All the find functions
// will NEVER return the head nodes.
func (s *Skiplist) findLast() *node {
	n := s.head
	level := int(s.getHeight()) - 1
	for {

View on GitHub (pinned to 2a001d466f)

Solutions

  1. Use the DB-level API with badger's own concurrency controls instead of sharing a raw skl.Skiplist across goroutines beyond its contract.
  2. Ensure keys have a strict total order (no key compares equal to another distinct key via a custom encoding).
  3. If this arises in stock badger, capture the repro and report it — it indicates corruption; reopen the DB from a clean state/WAL replay.
  4. Audit any local modifications to skl.go findSpliceForLevel/tower handling against upstream.

Example fix

// before
go list.Put(k, v1) // raw shared skiplist, no DB-level coordination
go list.Put(k2, v2) // can panic: Equality can happen only on base level
// after
db.Update(func(txn *Txn) error {
    txn.Set(k, v1); return nil
}) // let badger manage memtable/skiplist concurrency
Defensive patterns

Strategy: try-catch

Try / catch

defer func() {
	if r := recover(); r != nil {
		if strings.Contains(fmt.Sprint(r), "Equality can happen only on base level") {
			log.Printf("skiplist corruption suspected: %v", r)
			// reopen DB / rebuild memtable
		}
		panic(r)
	}
}()

Prevention

When it happens

Trigger: Concurrent Put operations racing such that findSpliceForLevel returns prev==next at a level above 0 — indicating either internal corruption or an out-of-contract use of the skiplist (e.g. key comparisons that violate total ordering, or direct concurrent misuse outside badger's guarantees).

Common situations: Highly concurrent writers to a directly-shared skl.Skiplist, custom comparators/key encodings where equal prefixes compare equal at higher levels, forks with modified node/tower logic, or memory corruption from buffer reuse.

Related errors


AI-assisted analysis of dgraph-io/badger@2a001d466f (2026-09-05). Data as JSON: /api/errors/907c8504a937c003. Report an issue: GitHub.