grpc/grpc-go · warning
RLS response's target list does not contain any entries for…
Error message
RLS response's target list does not contain any entries for key %+v
What it means
Per gRFC A24, an RLS response is considered failed if it returns OK status but an empty targets list. grpc-go enforces this in handleRouteLookupResponse (picker.go:319-320): a successful RPC with zero targets is converted into this synthetic error, which then drives the backoff state machine. The RPC that triggered the lookup is queued or routed to the default target.
Solutions
- Check the RLS server's routing table for the key shown in the error and add a target if one is expected.
- Configure a default target in the RLS LB config so RPCs can fall back when RLS returns no targets (useDefaultPickIfPossible at picker.go:231).
- Increase RLS cache maxAge/staleAge so transient empty responses do not evict good cached targets too aggressively.
Example fix
// before: RLS config with no default target
cfg := &rlspb.RouteLookupConfig{ GrpcKeybuilders: ..., LookupService: "rls.example" }
// after: provide a default target in the RLS LB policy child config so picks fall back
lbCfg := `{"childPolicy":[{"rls_experimental":{"lookupService":"rls.example","childPolicy":[{"pick_first":{}}],"defaultTarget":"default.backend:443"}}]}` Defensive patterns
Strategy: fallback
Prevention
- Configure a default target in the RLS LB policy so RPCs still succeed when RLS returns no targets.
- Monitor the RLS server's routing table coverage for the keys your clients send.
- Watch grpc.rls.request_count metrics for the "dropped" dimension to detect empty responses early.
When it happens
Trigger: The RLS server returns a RouteLookupResponse with targets=[] and no error. handleRouteLookupResponse synthesizes this error and stores it on the cache entry, scheduling backoff.
Common situations: The RLS server has no routing entry for the requested key (misconfigured upstream); the RLS server returns success but its own config is empty; a transient control-plane issue where the lookup matches no targets.
Related errors
- rls: method name is not of the form '/service/method
- rls: GrpcKeyBuilder in RouteLookupConfig contains repeated…
- rls: GrpcKeyBuilder in RouteLookupConfig contains a Name…
- rls: GrpcKeyBuilder in RouteLookupConfig contains repeated…
- rls: GrpcKeyBuilder in RouteLookupConfig contains repeated…
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/14b961b078af0c91.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/rls/picker.go:320
// - status is set to error returned from the control channel
// - current backoff state is available in the pending entry
// - `retries` field is incremented and
// - backoff state is moved to the data cache
// - backoffTime is set to the time indicated by the backoff state
// - backoffExpirationTime is set to twice the backoff time
// - backoffTimer is set to fire after backoffTime
//
// When a proactive cache refresh fails, this would leave the targets and the
// expiry time from the old entry unchanged. And this mean that the old valid
// entry would be used until expiration, and a new picker would be sent upon
// backoff expiry.
now := time.Now()
// "An RLS request is considered to have failed if it returns a non-OK
// status or the RLS response's targets list is non-empty." - RLS LB Policy
// design.
if len(targets) == 0 && err == nil {
err = fmt.Errorf("RLS response's target list does not contain any entries for key %+v", cacheKey)
// If err is set, rpc error from the control plane and no control plane
// configuration is why no targets were passed into this helper, no need
// to specify and tell the user this information.
}
if err != nil {
dcEntry.status = err
pendingEntry := p.lb.pendingMap[cacheKey]
pendingEntry.retries++
backoffTime := pendingEntry.bs.Backoff(pendingEntry.retries)
dcEntry.backoffState = pendingEntry
dcEntry.backoffTime = now.Add(backoffTime)
dcEntry.backoffExpiryTime = now.Add(2 * backoffTime)
if dcEntry.backoffState.timer != nil {
dcEntry.backoffState.timer.Stop()
}
dcEntry.backoffState.timer = time.AfterFunc(backoffTime, p.lb.sendNewPicker)
return
}View on GitHub (pinned to 0c51461d27)