grpc/grpc-go · error
outlier detection: child balancer
Error message
outlier detection: child balancer %q not registered
What it means
outlier_detection delegates actual load balancing to a child policy. In UpdateClientConnState (balancer.go:296-299) it resolves lbCfg.ChildPolicy.Name via balancer.Get; if no builder is registered, the config is rejected and not persisted, so the previous good config stays active.
Solutions
- Add a blank import for the missing balancer package, e.g. `import _ "google.golang.org/grpc/balancer/roundrobin"`.
- Confirm ChildPolicy.Name matches a registered balancer's Name() exactly.
- For custom balancers, ensure init() registers them before any grpc.Dial.
Example fix
// before
import _ "google.golang.org/grpc/balancer/roundrobin"
// outlier_detection config sets ChildPolicy.Name = "weighted_round_robin"
// after
import (
_ "google.golang.org/grpc/balancer/roundrobin"
_ "google.golang.org/grpc/balancer/weightedroundrobin"
) Defensive patterns
Strategy: validation
Validate before calling
func validateOutlierChildPolicyRegistered(name string) error {
if balancer.Get(name) == nil {
return fmt.Errorf("outlier_detection child policy %q is not registered; add a blank import", name)
}
return nil
} Prevention
- Blank-import every child balancer package your xDS deployment may route to.
- Cross-check ChildPolicy.Name against balancer.Get at startup.
- Document the supported child policy names in your service config schema.
When it happens
Trigger: lbCfg.ChildPolicy.Name references a balancer name for which no Builder was registered via balancer.Register.
Common situations: xDS control plane configures a child policy not compiled into the client; misspelled policy name; stripped gRPC build missing an optional balancer.
Related errors
- child policy not registered
- outlier detection: error switching to child of type
- xds: unable to unmarshal LBconfig
- failed to JSON marshal load balancing policy for child
- failed to parse load balancing policy for child
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/56c0ed1225df77c7.
Report an issue: GitHub.
Appendix: 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 0c51461d27)