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

  1. Remove WithSerializable from the put call; puts are always linearizable by definition
  2. Separate read options from write options in shared helpers (two explicit slices or two functions)
  3. If the flag arrived via a helper, add a lint/test that constructs every op combination your code builds
  4. 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

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


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