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

  1. Set max_age whenever you set stale_age, with stale_age <= max_age (e.g. max_age 300s, stale_age 240s).
  2. If you do not need background refresh, omit stale_age (or set it to zero) and rely on max_age alone.
  3. 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

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


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)