grpc/grpc-go · error

ringhash: expected xDS config selector to set the request…

Error message

ringhash: expected xDS config selector to set the request hash

What it means

Returned by the ringhash picker's Pick() (balancer/ringhash/picker.go:61) when requestHashHeader is empty (xDS-hash mode) but iringhash.XDSRequestHash(ctx) returns ok == false. In xDS mode, the request hash is expected to be injected into the RPC context by the xDS config selector so that the same client consistently picks the same endpoint. Its absence means the xDS pipeline did not set the hash.

Solutions

  1. Use ring_hash only within a full xDS-managed channel (xds.NewClientConn) so the config selector sets the hash.
  2. Alternatively, set requestHashHeader in the config so the picker hashes an outgoing metadata value instead of requiring xDS injection.
  3. Avoid interceptors that replace the RPC context without preserving the xDS hash.

Example fix

// before: plain grpc client with ring_hash and no requestHashHeader → xDS hash missing
conn, _ := grpc.NewClient("dns:///svc:50051", grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"ring_hash_experimental":{}}]}`))

// after: use an xDS-managed channel that injects the request hash, OR set requestHashHeader
conn, _ := xds.NewClientConn("xds:///svc", xds.WithServiceConfig(...))
// or, for non-xDS, provide a header-based hash:
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"ring_hash_experimental":{"requestHashHeader":"user-id"}}]}`)
Defensive patterns

Strategy: validation

Validate before calling

// Prefer a header-based hash in non-xDS setups to avoid the missing
// xDS-hash problem entirely.
func safeRingHashConfig() string {
    return `{"loadBalancingConfig":[{"ring_hash_experimental":{"requestHashHeader":"user-id"}}]}`
}

Try / catch

// If you cannot guarantee the xDS hash, catch the Unavailable and
// fall back to a header-based hash or a different policy.
resp, err := c.Call(ctx, in)
if err != nil && strings.Contains(err.Error(), "expected xDS config selector") {
    // reconfigure channel with requestHashHeader or switch policy
}
return resp, err

Prevention

When it happens

Trigger: The picker was built with requestHashHeader == "" (line 58), and at Pick time iringhash.XDSRequestHash(info.Ctx) returns (0, false) — no hash in the context. This happens when ring_hash is used through xDS but the config selector that normally sets the hash is absent or bypassed.

Common situations: Using ring_hash outside of a proper xDS configuration; the xDS RDS/RouteConfiguration lacks a hash policy; a custom interceptor that replaces the context drops the hash attribute; calling a ring_hash-backed channel directly without the xDS selector.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/78c3f3d29bb836c9. Report an issue: GitHub.

Appendix: source

Thrown at balancer/ringhash/picker.go:61

	// requestHashHeader is the header key to look for the request hash. If it's
	// empty, the request hash is expected to be set in the context via xDS.
	// See gRFC A76.
	requestHashHeader string

	// hasEndpointInConnectingState is true if any of the endpoints is in
	// CONNECTING.
	hasEndpointInConnectingState bool

	randUint64 func() uint64
}

func (p *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
	usingRandomHash := false
	var requestHash uint64
	if p.requestHashHeader == "" {
		var ok bool
		if requestHash, ok = iringhash.XDSRequestHash(info.Ctx); !ok {
			return balancer.PickResult{}, fmt.Errorf("ringhash: expected xDS config selector to set the request hash")
		}
	} else {
		md, ok := metadata.FromOutgoingContext(info.Ctx)
		if !ok || len(md.Get(p.requestHashHeader)) == 0 {
			requestHash = p.randUint64()
			usingRandomHash = true
		} else {
			values := strings.Join(md.Get(p.requestHashHeader), ",")
			requestHash = xxhash.Sum64String(values)
		}
	}

	e := p.ring.pick(requestHash)
	ringSize := len(p.ring.items)
	if !usingRandomHash {
		// Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF,
		// we ignore all TF subchannels and find the first ring entry in READY,
		// CONNECTING or IDLE.  If that entry is in IDLE, we need to initiate a

View on GitHub (pinned to 0c51461d27)