{"record":{"id":"395a73128c206cde","repo":"nats-io/nats-server","slug":"max-age-needs-to-be-100ms","errorCode":null,"errorMessage":"max age needs to be >= 100ms","messagePattern":"max age needs to be >= 100ms","errorType":"error_code","errorClass":"JSStreamInvalidConfigError","httpStatus":null,"severity":"error","filePath":"server/stream.go","lineNumber":1954,"sourceCode":"\t\tcfg.MaxBytes = -1\n\t}\n\tif cfg.MaxMsgSize == 0 || cfg.MaxMsgSize < -1 {\n\t\tif pedantic && cfg.MaxMsgSize < -1 {\n\t\t\treturn StreamConfig{}, NewJSPedanticError(fmt.Errorf(\"max_msg_size must be set to -1\"))\n\t\t}\n\t\tcfg.MaxMsgSize = -1\n\t}\n\tif cfg.MaxConsumers == 0 || cfg.MaxConsumers < -1 {\n\t\tif pedantic && cfg.MaxConsumers < -1 {\n\t\t\treturn StreamConfig{}, NewJSPedanticError(fmt.Errorf(\"max_consumers must be set to -1\"))\n\t\t}\n\t\tcfg.MaxConsumers = -1\n\t}\n\tif cfg.MaxAge < 0 {\n\t\treturn StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf(\"max age can not be negative\"))\n\t}\n\tif cfg.MaxAge != 0 && cfg.MaxAge < 100*time.Millisecond {\n\t\treturn StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf(\"max age needs to be >= 100ms\"))\n\t}\n\n\tif cfg.Duplicates == 0 && cfg.Mirror == nil && len(cfg.Sources) == 0 {\n\t\tmaxWindow := StreamDefaultDuplicatesWindow\n\t\tif lim.Duplicates > 0 && maxWindow > lim.Duplicates {\n\t\t\tif pedantic {\n\t\t\t\treturn StreamConfig{}, NewJSPedanticError(fmt.Errorf(\"duplicate window limits are higher than current limits\"))\n\t\t\t}\n\t\t\tmaxWindow = lim.Duplicates\n\t\t}\n\t\tif cfg.MaxAge != 0 && cfg.MaxAge < maxWindow {\n\t\t\tif pedantic {\n\t\t\t\treturn StreamConfig{}, NewJSPedanticError(fmt.Errorf(\"duplicate window cannot be bigger than max age\"))\n\t\t\t}\n\t\t\tcfg.Duplicates = cfg.MaxAge\n\t\t} else {\n\t\t\tcfg.Duplicates = maxWindow\n\t\t}","sourceCodeStart":1936,"sourceCodeEnd":1972,"githubUrl":"https://github.com/nats-io/nats-server/blob/3a66a489d262bf89b71a71c955c94920394532f3/server/stream.go#L1936-L1972","documentation":"JetStream requires MaxAge, when set, to be at least 100ms; shorter values are rejected with JSStreamInvalidConfigError. This enforces a sane lower bound on message retention so the server's internal expiry machinery behaves correctly.","triggerScenarios":"Stream create/update with 0 < MaxAge < 100*time.Millisecond, e.g. MaxAge: 50*time.Millisecond or a duration computed in the wrong time unit (seconds vs milliseconds confusion). server/stream.go:1954.","commonSituations":"Unit conversion bugs (intended 100s but wrote 100*time.Millisecond); tests using tiny TTLs to speed up expiry; dynamic configs from user input with millisecond inputs intended as seconds.","solutions":["Raise MaxAge to at least 100*time.Millisecond","If you truly need short retention, keep a floor of 100ms in your config layer","Check unit conversions (ms vs s) in code that builds the duration"],"exampleFix":"// before\nMaxAge: 50 * time.Millisecond\n// after\nMaxAge: 100 * time.Millisecond","handlingStrategy":"validation","validationCode":"if cfg.MaxAge != 0 && cfg.MaxAge < 100*time.Millisecond {\n    return fmt.Errorf(\"MaxAge must be >= 100ms; got %v\", cfg.MaxAge)\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    var apiErr *nats.APIError\n    if errors.As(err, &apiErr) && apiErr.ErrorCode == nats.ErrorCodeJetStreamInvalidStreamConfig {\n        cfg.MaxAge = 100 * time.Millisecond\n        err = createStream(cfg)\n    }\n}","preventionTips":["Enforce a 100ms floor constant in your config layer","Review unit conversions (ms vs seconds) wherever durations are built","Avoid sub-100ms retention even in tests; scale tests differently"],"tags":["jetstream","stream-config","validation","duration"],"backgroundTag":"jetstream-invalid-stream-config","analyzedSha":"3a66a489d262bf89b71a71c955c94920394532f3","analyzedAt":"2026-09-02T04:41:54.247Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}