etcd-io/etcd · error

unexpected revision = 0. Calling SyncUpdates before…

Error message

unexpected revision = 0. Calling SyncUpdates before SyncBase finishes?

What it means

mirror.NewSyncer(...).SyncBase(ctx) returns channels; the syncer records the latest revision it saw as s.rev. SyncUpdates(ctx) starts a watch from s.rev+1 and panics if s.rev is still 0, i.e. when no revision has been captured yet. The panic message itself asks the diagnostic question: SyncUpdates was called before SyncBase had delivered (and the syncer processed) its first response, so there is no valid revision to resume from.

Solutions

  1. Wait until SyncBase has delivered at least one response (or completed) before calling SyncUpdates — read from the SyncBase channels in the same goroutine that then calls SyncUpdates.
  2. Run the full mirror pattern in one goroutine: consume baseChan/errChan to exhaustion, then start SyncUpdates (see the documented Mirror example in clientv3/mirror).
  3. If SyncBase's context was cancelled or errored, create a fresh syncer via mirror.NewSyncer instead of continuing with the spent one.
  4. As a structural guard, wrap SyncUpdates invocation with a check that SyncBase finished (e.g. signal channel closed in your consumer).

Example fix

// before
baseChan, errChan := syncer.SyncBase(ctx)
go func() { for range baseChan {} }() // not drained / racing
upChan := syncer.SyncUpdates(ctx) // panics: unexpected revision = 0

// after — mirror pattern: drain SyncBase fully, then SyncUpdates
baseChan, errChan := syncer.SyncBase(ctx)
for resp := range baseChan {
    handle(resp) // syncer records rev from these responses
}
for err := range errChan {
    if err != nil { return err }
}
upChan := syncer.SyncUpdates(ctx) // safe: rev > 0
Defensive patterns

Strategy: validation

Validate before calling

// structural guard: only start SyncUpdates after SyncBase channels are exhausted
baseCh, errCh := syncer.SyncBase(ctx)
for r := range baseCh { handle(r) }
for e := range errCh {
    if e != nil { return e }
}
// safe point: syncer.rev > 0
upCh := syncer.SyncUpdates(ctx)

Prevention

When it happens

Trigger: Calling s.SyncUpdates(ctx) immediately after s.SyncBase(ctx) without waiting for any kv or header to arrive on the returned channels; SyncBase whose context is cancelled before the first range completes; an empty keyspace is fine (the header still carries a revision) but a failed/interrupted initial sync leaves rev==0.

Common situations: Race at service startup: goroutine consuming SyncUpdates starts before the SyncBase goroutine receives the first response; cancelling SyncBase's context on shutdown and then (re)starting SyncUpdates on the same syncer; reusing a syncer after an error instead of creating a new one.

Related errors


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/40258aec7ab44ea9. Report an issue: GitHub.

Appendix: source

Thrown at client/v3/mirror/syncer.go:117

				return
			}

			respchan <- resp

			if !resp.More {
				return
			}
			// move to next key
			key = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0))
		}
	}()

	return respchan, errchan
}

func (s *syncer) SyncUpdates(ctx context.Context) clientv3.WatchChan {
	if s.rev == 0 {
		panic("unexpected revision = 0. Calling SyncUpdates before SyncBase finishes?")
	}
	return s.c.Watch(ctx, s.prefix, clientv3.WithPrefix(), clientv3.WithRev(s.rev+1))
}

View on GitHub (pinned to f744d457f4)