{"record":{"id":"1c97de45cfe6435b","repo":"router-for-me/CLIProxyAPI","slug":"credential-in-flight-stale-after-must-be-at-least","errorCode":null,"errorMessage":"credential-in-flight.stale-after must be at least three snapshot intervals","messagePattern":"credential-in-flight\\.stale-after must be at least three snapshot intervals","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/credential_in_flight.go","lineNumber":53,"sourceCode":"\t\tMaxPartBytes:       DefaultInFlightMaxPartBytes,\n\t\tMaxPartCount:       DefaultInFlightMaxPartCount,\n\t\tMaxRevisionBytes:   DefaultInFlightMaxRevisionBytes,\n\t\tMaxAggregateGroups: DefaultInFlightMaxAggregateGroups,\n\t\tMaxDetails:         DefaultInFlightMaxDetails,\n\t\tMaxStringBytes:     DefaultInFlightMaxStringBytes,\n\t\tStagingRetention:   \"1m\",\n\t}\n}\n\n// Durations parses and validates the in-flight observation durations.\nfunc (c CredentialInFlightConfig) Durations() (time.Duration, time.Duration, time.Duration, error) {\n\tsnapshotInterval, errSnapshot := time.ParseDuration(c.SnapshotInterval)\n\tif errSnapshot != nil || snapshotInterval <= 0 {\n\t\treturn 0, 0, 0, fmt.Errorf(\"credential-in-flight.snapshot-interval must be positive\")\n\t}\n\tstaleAfter, errStale := time.ParseDuration(c.StaleAfter)\n\tif errStale != nil || staleAfter <= 0 || snapshotInterval > staleAfter/3 {\n\t\treturn 0, 0, 0, fmt.Errorf(\"credential-in-flight.stale-after must be at least three snapshot intervals\")\n\t}\n\tstagingRetention, errRetention := time.ParseDuration(c.StagingRetention)\n\tif errRetention != nil || stagingRetention <= 0 {\n\t\treturn 0, 0, 0, fmt.Errorf(\"credential-in-flight.staging-retention must be positive\")\n\t}\n\treturn snapshotInterval, staleAfter, stagingRetention, nil\n}\n\n// Validate verifies the in-flight observation bounds.\nfunc (c CredentialInFlightConfig) Validate() error {\n\tif _, _, _, errDurations := c.Durations(); errDurations != nil {\n\t\treturn errDurations\n\t}\n\tif c.MaxPartBytes < 1024 || c.MaxPartCount <= 0 || c.MaxPartCount > DefaultInFlightMaxPartCount {\n\t\treturn fmt.Errorf(\"credential-in-flight part bounds are invalid\")\n\t}\n\tif c.MaxRevisionBytes < c.MaxPartBytes || c.MaxRevisionBytes > DefaultInFlightMaxRevisionBytes {\n\t\treturn fmt.Errorf(\"credential-in-flight.max-revision-bytes is outside hard bounds\")","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/config/credential_in_flight.go#L35-L71","documentation":"CredentialInFlightConfig.Durations() requires stale-after to parse, be positive, and be at least three times snapshot-interval (snapshotInterval > staleAfter/3 fails). The 3x headroom guarantees a credential is only marked stale after several missed snapshots, tolerating transient hiccups without flapping in-flight accounting.","triggerScenarios":"config.yaml with 'credential-in-flight:\\n  snapshot-interval: 2s\\n  stale-after: 5s' (5s/3 = 1.67s < 2s, fails), or stale-after: 0s, negative, or unparseable. Needs snapshot-interval valid first, since it is checked before this.","commonSituations":"Tightening stale-after for faster reaping while forgetting the 3x rule; setting stale-after equal to snapshot-interval; copy-pasting a stale-after from an older config with a different snapshot cadence.","solutions":["Set stale-after to at least 3x snapshot-interval, e.g. snapshot-interval: 2s with stale-after: 10s (documented default).","If you shrink snapshot-interval, scale stale-after down proportionally but keep the ratio >= 3.","Verify both strings parse as Go durations (no spaces, valid units)."],"exampleFix":"# before (config.yaml)\ncredential-in-flight:\n  snapshot-interval: 2s\n  stale-after: 5s\n\n# after\ncredential-in-flight:\n  snapshot-interval: 2s\n  stale-after: 10s","handlingStrategy":"validation","validationCode":"// Go: enforce the 3x rule up front.\nsi, _ := time.ParseDuration(cfg.CredentialInFlight.SnapshotInterval)\nsa, err := time.ParseDuration(cfg.CredentialInFlight.StaleAfter)\nif err != nil || sa <= 0 || si > sa/3 {\n    return errors.New(\"stale-after must be >= 3 * snapshot-interval\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Change snapshot-interval and stale-after together, preserving the >= 3x ratio.","Default pair 2s/10s satisfies the rule; use it as the anchor."],"tags":["config","validation","credential-in-flight","durations","observability"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}