grpc/grpc-go · error

outlier detection: child balancer %q not registered

Error message

outlier detection: child balancer %q not registered

What it means

Returned by outlier detection UpdateClientConnState (internal/xds/balancer/outlierdetection/balancer.go:297) when `balancer.Get(lbCfg.ChildPolicy.Name)` returns nil — i.e. the child LB policy named in the config is not a registered balancer. Outlier detection wraps a child policy and rejects the whole config up front if the child cannot be built.

Source

Thrown at internal/xds/balancer/outlierdetection/balancer.go:298

			b.unejectEndpoint(epInfo)
		}
		// "Reset each endpoint's ejection time multiplier to 0."
		epInfo.ejectionTimeMultiplier = 0
	}
}

func (b *outlierDetectionBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
	lbCfg, ok := s.BalancerConfig.(*LBConfig)
	if !ok {
		b.logger.Errorf("received config with unexpected type %T: %v", s.BalancerConfig, s.BalancerConfig)
		return balancer.ErrBadResolverState
	}

	// Reject whole config if child policy doesn't exist, don't persist it for
	// later.
	bb := balancer.Get(lbCfg.ChildPolicy.Name)
	if bb == nil {
		return fmt.Errorf("outlier detection: child balancer %q not registered", lbCfg.ChildPolicy.Name)
	}

	// It is safe to read b.cfg here without holding the mutex, as the only
	// write to b.cfg happens later in this function. This function is part of
	// the balancer.Balancer API, so it is guaranteed to be called in a
	// synchronous manner, so it cannot race with this read.
	if b.cfg == nil || b.cfg.ChildPolicy.Name != lbCfg.ChildPolicy.Name {
		if err := b.child.switchTo(bb); err != nil {
			return fmt.Errorf("outlier detection: error switching to child of type %q: %v", lbCfg.ChildPolicy.Name, err)
		}
	}

	b.mu.Lock()
	// Inhibit child picker updates until this UpdateClientConnState() call
	// completes. If needed, a picker update containing the no-op config bit
	// determined from this config and most recent state from the child will be
	// sent synchronously upward at the end of this UpdateClientConnState()
	// call.

View on GitHub (pinned to 03255a9237)

Solutions

  1. Ensure the child policy name matches a registered balancer (e.g. round_robin, weighted_target)
  2. Import the package of any custom child balancer so its init() registers it
  3. Fix the control plane to only send policy names the client supports

Example fix

// before: custom policy never registered, config references "acme_lb"
// after
import _ "example.com/acme/grpcbal/acmelb" // registers "acme_lb" via balancer.Register in init()
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the child policy is a registered balancer before applying.
if lbCfg.ChildPolicy == nil || balancer.Get(lbCfg.ChildPolicy.Name) == nil {
    return fmt.Errorf("child balancer %q is not registered", childPolicyName(lbCfg))
}

Type guard

func hasRegisteredChild(cfg *outlierdetection.LBConfig) bool {
    return cfg != nil && cfg.ChildPolicy != nil && balancer.Get(cfg.ChildPolicy.Name) != nil
}

Try / catch

if err := bal.UpdateClientConnState(state); err != nil {
    if strings.Contains(err.Error(), "not registered") {
        logger.Errorf("child policy unavailable; keeping previous config: %v", err)
    }
    return err
}

Prevention

When it happens

Trigger: UpdateClientConnState is called with an LBConfig whose ChildPolicy.Name is not a registered balancer builder (e.g. "my_custom_lb" when only stock policies are registered, or a typo like "roud_robin").

Common situations: Control plane references an LB policy the binary doesn't support; a custom LB policy wasn't registered via balancer.Register or its init() wasn't imported; version skew where a newer policy name reaches an older client.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/56c0ed1225df77c7. Report an issue: GitHub.