{"record":{"id":"a737b84843617d82","repo":"grpc/grpc-go","slug":"rls-stale-age-is-set-but-max-age-is-not-in-route","errorCode":null,"errorMessage":"rls: stale_age is set, but max_age is not in route lookup config %+v","messagePattern":"rls: stale_age is set, but max_age is not in route lookup config %\\+v","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"balancer/rls/config.go","lineNumber":249,"sourceCode":"\tif maxAge == 0 {\n\t\tmaxAge = maxMaxAge\n\t} else {\n\t\tmaxAgeSet = true\n\t}\n\n\tstaleAgeSet := false\n\tstaleAge, err := convertDuration(rlsProto.GetStaleAge())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rls: failed to parse staleAge in route lookup config %+v: %v\", rlsProto, err)\n\t}\n\tif staleAge == 0 {\n\t\tstaleAge = maxMaxAge\n\t} else {\n\t\tstaleAgeSet = true\n\t}\n\n\tif staleAgeSet && !maxAgeSet {\n\t\treturn nil, fmt.Errorf(\"rls: stale_age is set, but max_age is not in route lookup config %+v\", rlsProto)\n\t}\n\tif staleAge > maxMaxAge {\n\t\tstaleAge = maxMaxAge\n\t}\n\tif !staleAgeSet && maxAge > maxMaxAge {\n\t\tmaxAge = maxMaxAge\n\t}\n\tif staleAge > maxAge {\n\t\tstaleAge = maxAge\n\t}\n\n\t// `cache_size_bytes` field must have a value greater than 0, and if its\n\t// value is greater than 5M, we cap it at 5M\n\tcacheSizeBytes := rlsProto.GetCacheSizeBytes()\n\tif cacheSizeBytes <= 0 {\n\t\treturn nil, fmt.Errorf(\"rls: cache_size_bytes must be set to a non-zero value: %+v\", rlsProto)\n\t}\n\tif cacheSizeBytes > maxCacheSize {","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/balancer/rls/config.go#L231-L267","documentation":"The RLS data cache uses two timers: max_age (hard expiry, evicts the entry) and stale_age (soft expiry, triggers a background refresh while still serving the stale entry). Per the RLS LB policy design, stale_age is only meaningful relative to max_age, so gRPC refuses a RouteLookupConfig that sets stale_age without also setting max_age. The check happens at service-config parse time in parseRLSProto.","triggerScenarios":"A RouteLookupConfig where max_age is unset/zero but stale_age is a non-zero duration. Produced by passing a service config JSON like {\"routeLookupConfig\":{...\"staleAge\":\"30s\"...}} with no \"maxAge\" field, or a proto where GetMaxAge() is nil/0s and GetStaleAge() is set.","commonSituations":"Copying a partial RLS example that omits max_age; templated configs that conditionally inject stale_age but forget max_age; assuming max_age has a useful default when stale_age is supplied (it does default to 5m, but only when stale_age is also unset).","solutions":["Add a max_age field to the same RouteLookupConfig with a value greater than stale_age (e.g. \"maxAge\":\"300s\" paired with \"staleAge\":\"240s\").","If you do not need background refresh, remove the stale_age field entirely so the config uses the default cache behavior.","Remember max_age and stale_age are both capped at 5 minutes; values above 5m are silently clamped, so keep max_age <= 300s."],"exampleFix":"// before (service config JSON fragment)\n\"routeLookupConfig\": {\n  \"lookupService\": \"rls.example:443\",\n  \"staleAge\": \"240s\",\n  \"cacheSizeBytes\": 1000000\n}\n// after\n\"routeLookupConfig\": {\n  \"lookupService\": \"rls.example:443\",\n  \"maxAge\": \"300s\",\n  \"staleAge\": \"240s\",\n  \"cacheSizeBytes\": 1000000\n}","handlingStrategy":"validation","validationCode":"func validateRLSAges(cfg *rlspb.RouteLookupConfig) error {\n    maxSet := cfg.GetMaxAge() != nil && cfg.GetMaxAge().AsDuration() != 0\n    staleSet := cfg.GetStaleAge() != nil && cfg.GetStaleAge().AsDuration() != 0\n    if staleSet && !maxSet {\n        return fmt.Errorf(\"stale_age set without max_age\")\n    }\n    if maxSet && cfg.GetMaxAge().AsDuration() > 5*time.Minute {\n        fmt.Println(\"note: max_age > 5m will be clamped to 5m\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always set maxAge and staleAge together in templated RLS configs.","Keep a JSON schema for your RLS service config and require maxAge whenever staleAge is present.","Run the config through rls.ParseConfig (or an equivalent validator) in CI before deploying."],"tags":["rls","service-config","config-validation","go","grpc","load-balancing"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}