etcd-io/etcd · error
unexpected serializable in put
Error message
unexpected serializable in put
What it means
OpPut panics with 'unexpected serializable in put' when WithSerializable is applied to a put. Serializable reads control whether a Get can be served by a possibly-stale follower; writes must always go through quorum, so the flag is rejected for puts. The panic is a fail-fast guard inside OpPut executed at construction time.
Source
Thrown at client/v3/op.go:314
}
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
}
// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {
clonedCmps := make([]Cmp, len(cmps))
for i := range cmps {View on GitHub (pinned to f744d457f4)
Solutions
- Remove WithSerializable from the put call; puts are always linearizable by definition
- Separate read options from write options in shared helpers (two explicit slices or two functions)
- If the flag arrived via a helper, add a lint/test that constructs every op combination your code builds
- Check for other read-only flags in the same options slice since sibling panics will fire next
Example fix
// before
readOpts := []clientv3.OpOption{clientv3.WithSerializable()}
op := clientv3.OpPut("k", "v", readOpts...)
// after
readOpts := []clientv3.OpOption{clientv3.WithSerializable()}
op := clientv3.OpGet("k", readOpts...)
putOp := clientv3.OpPut("k", "v") Defensive patterns
Strategy: validation
Validate before calling
// Keep serializable scoped to reads: two explicit builders, never one shared slice.
func readOpts() []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithSerializable()} // Get only
}
func writeOpts() []clientv3.OpOption {
return nil // puts take no consistency flags
} Try / catch
defer func() {
if r := recover(); r != nil {
if msg, ok := r.(string); ok && strings.Contains(msg, "in put") {
log.Printf("misconfigured put options: %s", msg)
}
}
}() Prevention
- Treat WithSerializable as read-only tuning; never blanket-apply it
- Review central option factories for flags applied to every call
- Document which options are valid per operation type in your helpers
When it happens
Trigger: Calling clientv3.OpPut(key, val, clientv3.WithSerializable()). Real-world route: an options variable built for a Get (e.g. opts := []clientv3.OpOption{clientv3.WithSerializable()}) reused for a Put, or a kv.Put(ctx, key, val, opts...) style helper that forwards read options to OpPut.
Common situations: Teams standardizing on a shared OpOption slice for 'weak consistency' reads that is accidentally passed to writes; migrating code from clientv3.KV.Get (which accepts WithSerializable) to explicit Op building; version upgrades where helper signatures widened to accept variadic options.
Related errors
- unexpected sort in put
- unexpected countOnly in put
- unexpected mod revision filter in put
- unexpected create revision filter in put
- unexpected filter in put
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/a54a451cac457379.
Report an issue: GitHub.