grpc/grpc-go · error
rls: stale_age is set, but max_age is not in route lookup…
Error message
rls: stale_age is set, but max_age is not in route lookup config %+v
What it means
Emitted from parseRLSProto (config.go:249) when stale_age is explicitly set (non-zero) in the RouteLookupConfig but max_age is not. Background refresh (stale) semantics require a hard expiry (max) to be defined, so the config is rejected. Zero stale_age skips this check.
Solutions
- Set max_age whenever you set stale_age, with stale_age <= max_age (e.g. max_age 300s, stale_age 240s).
- If you do not need background refresh, omit stale_age (or set it to zero) and rely on max_age alone.
- Fix the control-plane template so the two duration fields are emitted as a pair.
Example fix
// before
"routeLookupConfig": { "maxAge": null, "staleAge": { "seconds": 240 }, ... }
// after
"routeLookupConfig": { "maxAge": { "seconds": 300 }, "staleAge": { "seconds": 240 }, ... } Defensive patterns
Strategy: validation
Validate before calling
func validateAgePair(maxAge, staleAge *durationpb.Duration) error {
if staleAge == nil {
return nil
}
if staleAge.AsDuration() != 0 && (maxAge == nil || maxAge.AsDuration() == 0) {
return errors.New("stale_age is set but max_age is not; provide a non-zero max_age")
}
return nil
} Type guard
func agePairIsConsistent(maxAge, staleAge *durationpb.Duration) bool {
if staleAge == nil || staleAge.AsDuration() == 0 {
return true
}
return maxAge != nil && maxAge.AsDuration() != 0
} Prevention
- Treat max_age and stale_age as a pair in config templates and schema.
- Default stale_age to unset unless background refresh is explicitly wanted.
- Add a config-lint rule that rejects stale_age-without-max_age in CI.
When it happens
Trigger: routeLookupConfig provides staleAge (non-zero) and omits maxAge or sets it to zero.
Common situations: An operator sets stale_age for background refresh but assumes max_age has a default; xDS RLS resource that only partially populates the duration fields; a config-generation bug that writes stale_age conditionally but max_age unconditionally-as-zero.
Related errors
- rls: bad control channel service config
- rls: bad RouteLookupConfig proto %+v
- rls: cache_size_bytes must be set to a non-zero value: %+v
- rls: childPolicyConfigTargetFieldName field is not set in…
- rls: empty lookup_service in route lookup config %+v
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/a737b84843617d82.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/rls/config.go:249
if maxAge == 0 {
maxAge = maxMaxAge
} else {
maxAgeSet = true
}
staleAgeSet := false
staleAge, err := convertDuration(rlsProto.GetStaleAge())
if err != nil {
return nil, fmt.Errorf("rls: failed to parse staleAge in route lookup config %+v: %v", rlsProto, err)
}
if staleAge == 0 {
staleAge = maxMaxAge
} else {
staleAgeSet = true
}
if staleAgeSet && !maxAgeSet {
return nil, fmt.Errorf("rls: stale_age is set, but max_age is not in route lookup config %+v", rlsProto)
}
if staleAge > maxMaxAge {
staleAge = maxMaxAge
}
if !staleAgeSet && maxAge > maxMaxAge {
maxAge = maxMaxAge
}
if staleAge > maxAge {
staleAge = maxAge
}
// `cache_size_bytes` field must have a value greater than 0, and if its
// value is greater than 5M, we cap it at 5M
cacheSizeBytes := rlsProto.GetCacheSizeBytes()
if cacheSizeBytes <= 0 {
return nil, fmt.Errorf("rls: cache_size_bytes must be set to a non-zero value: %+v", rlsProto)
}
if cacheSizeBytes > maxCacheSize {View on GitHub (pinned to 0c51461d27)