etcd-io/etcd · error

unexpected revision in put

Error message

unexpected revision in put

What it means

OpPut rejects WithRev(n). WithRev pins a range read to a historical MVCC revision; writes always apply to the latest state, so a revision-pinned put is contradictory. When applyOpts leaves ret.rev non-zero, the constructor's validation switch panics with "unexpected revision in put".

Source

Thrown at client/v3/op.go:310

	case ret.filterDelete, ret.filterPut:
		panic("unexpected filter in delete")
	case ret.createdNotify:
		panic("unexpected createdNotify in delete")
	}
	return ret
}

// OpPut returns "put" operation based on given key-value and operation options.
func OpPut(key, val string, opts ...OpOption) Op {
	ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
	ret.applyOpts(opts)
	switch {
	case ret.end != nil:
		panic("unexpected range in put")
	case ret.limit != 0:
		panic("unexpected limit in put")
	case ret.rev != 0:
		panic("unexpected revision in put")
	case ret.sort != nil:
		panic("unexpected sort in put")
	case ret.serializable:
		panic("unexpected serializable in put")
	case ret.countOnly:
		panic("unexpected countOnly in put")
	case ret.minModRev != 0, ret.maxModRev != 0:
		panic("unexpected mod revision filter in put")
	case ret.minCreateRev != 0, ret.maxCreateRev != 0:
		panic("unexpected create revision filter in put")
	case ret.filterDelete, ret.filterPut:
		panic("unexpected filter in put")
	case ret.createdNotify:
		panic("unexpected createdNotify in put")
	}
	return ret
}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithRev from Put calls.
  2. For 'put only if unchanged since revision R', use cli.Txn(ctx).If(clientv3.Compare(clientv3.CompareModified(k), "=", R)).Then(clientv3.OpPut(k, v)).Commit() and check resp.Succeeded.
  3. Keep snapshot-read options separate from write options.

Example fix

// before
_, err := cli.Put(ctx, k, newVal, clientv3.WithRev(seenRev)) // panics: unexpected revision in put

// after — optimistic write guarded by the observed revision
resp, err := cli.Txn(ctx).
    If(clientv3.Compare(clientv3.CompareModified(k), "=", seenRev)).
    Then(clientv3.OpPut(k, newVal)).
    Commit()
if !resp.Succeeded { /* concurrent modification: re-read and retry */ }
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: clientv3.OpPut("/k", "v", clientv3.WithRev(42)); read-modify-write flows that reuse the historical Get's options (WithRev for snapshot reads) on the write-back Put; watch-replay code carrying event revisions into re-puts.

Common situations: Optimistic-concurrency attempts: developers pin the Get to a revision and try to 'put at that revision' — the correct etcd primitive is a Txn If with CompareModified/CompareCreateRevision; option-slice reuse between snapshot reads and writes.

Related errors


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