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

  1. Add a blank import for the missing balancer package, e.g. `import _ "google.golang.org/grpc/balancer/roundrobin"`.
  2. Confirm ChildPolicy.Name matches a registered balancer's Name() exactly.
  3. 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

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


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)