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
- Remove WithRev from Put calls.
- 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.
- 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
- Writes always target latest state; WithRev is only for historical reads.
- For optimistic concurrency use Txn If(Compare(CompareModified(k), "=", rev)) Then(OpPut(...)) and check resp.Succeeded.
- Do not carry a snapshot Get's options into the write-back Put.
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
- unexpected revision in delete
- unexpected range in put
- unexpected limit in put
- unexpected lease in delete
- unexpected limit in delete
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/318546736bc33f00.
Report an issue: GitHub.