{"record":{"id":"56d9957fe186b4a8","repo":"n8n-io/n8n","slug":"key-must-be-a-positive-number-of-seconds-got","errorCode":null,"errorMessage":"${key} must be a positive number of seconds, got ${value}","messagePattern":"(.+?) must be a positive number of seconds, got (.+?)","errorType":"validation","errorClass":"InvalidLifecycleOptionsError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/scheduler/src/core/factory.ts","lineNumber":166,"sourceCode":"\t}\n\n\t// A non-positive or NaN interval collapses to setTimeout's 1ms floor,\n\t// turning the pass into a hot loop against task storage.\n\t// A non-positive or NaN timeout would abandon every pass the moment it starts.\n\tconst durationKeys = [\n\t\t'materializerIntervalSeconds',\n\t\t'executorIntervalSeconds',\n\t\t'reaperIntervalSeconds',\n\t\t'retentionIntervalSeconds',\n\t\t'materializerTimeoutSeconds',\n\t\t'executorTimeoutSeconds',\n\t\t'reaperTimeoutSeconds',\n\t\t'retentionTimeoutSeconds',\n\t] as const;\n\tfor (const key of durationKeys) {\n\t\tconst value = lifecycleOptions[key];\n\t\tif (!(Number.isFinite(value) && value > 0)) {\n\t\t\tthrow new InvalidLifecycleOptionsError(\n\t\t\t\t`${key} must be a positive number of seconds, got ${value}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (\n\t\tlifecycleOptions.concurrencyMode !== 'sequential' &&\n\t\tlifecycleOptions.concurrencyMode !== 'concurrent'\n\t) {\n\t\tthrow new InvalidLifecycleOptionsError(\n\t\t\t`concurrencyMode must be 'sequential' or 'concurrent', got ${String(lifecycleOptions.concurrencyMode)}`,\n\t\t);\n\t}\n\n\tconst { maxConcurrentPasses } = lifecycleOptions;\n\tif (!(Number.isInteger(maxConcurrentPasses) && maxConcurrentPasses >= 1)) {\n\t\tthrow new InvalidLifecycleOptionsError(\n\t\t\t`maxConcurrentPasses must be a positive integer, got ${maxConcurrentPasses}`,","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scheduler/src/core/factory.ts#L148-L184","documentation":"Thrown by createScheduler when any of eight lifecycle duration options (materializer/executor/reaper/retention Interval*Seconds and Timeout*Seconds) is not a positive finite number. The comment above the loop explains why: a non-positive or NaN interval collapses to setTimeout's 1ms floor and turns the scheduler pass into a hot loop against task storage; a non-positive timeout abandons every pass immediately.","triggerScenarios":"Passing lifecycle.materializerIntervalSeconds: 0, a negative number, NaN, Infinity, or undefined-after-defaults. Each of the eight keys is checked with Number.isFinite(value) && value > 0.","commonSituations":"An env var that is empty string (Number('') === 0); a config that omits a timeout so it defaults to undefined and withDefaults leaves it undefined; passing Infinity from a 'never timeout' intent; floats that drift to 0 after integer casting upstream.","solutions":["Set each interval and timeout to a positive finite number of seconds (e.g. materializerIntervalSeconds: 1, executorTimeoutSeconds: 30).","Express 'no timeout' as a large finite number, never Infinity.","Validate with Number.isFinite(x) && x > 0 in your config loader before createScheduler.","Let withDefaults fill values by omitting the keys entirely rather than passing 0 or undefined."],"exampleFix":"// before\ncreateScheduler({ lifecycle: { materializerIntervalSeconds: 0, executorTimeoutSeconds: NaN } }); // throws\n\n// after - sanitize config-derived values\nfunction positiveSeconds(raw: unknown, fallback: number): number {\n  const n = Number(raw);\n  return Number.isFinite(n) && n > 0 ? n : fallback;\n}\ncreateScheduler({\n  lifecycle: {\n    materializerIntervalSeconds: positiveSeconds(cfg.materInterval, 1),\n    executorTimeoutSeconds: positiveSeconds(cfg.execTimeout, 30),\n  },\n});","handlingStrategy":"validation","validationCode":"const DURATION_KEYS = [\n  'materializerIntervalSeconds', 'executorIntervalSeconds',\n  'reaperIntervalSeconds', 'retentionIntervalSeconds',\n  'materializerTimeoutSeconds', 'executorTimeoutSeconds',\n  'reaperTimeoutSeconds', 'retentionTimeoutSeconds',\n] as const;\n\nfunction sanitizeDurations(opts: Record<string, unknown>): Record<string, number> {\n  const out: Record<string, number> = {};\n  for (const key of DURATION_KEYS) {\n    const n = Number(opts[key]);\n    if (!(Number.isFinite(n) && n > 0)) throw new Error(`${key} must be a positive finite number`);\n    out[key] = n;\n  }\n  return out;\n}","typeGuard":"function isPositiveSeconds(v: unknown): v is number {\n  return typeof v === 'number' && Number.isFinite(v) && v > 0;\n}","tryCatchPattern":null,"preventionTips":["Treat 'no timeout' as a large finite number, never Infinity.","Validate env-var-derived values with Number.isFinite && > 0 before passing to createScheduler.","Let withDefaults fill values by omitting keys rather than passing 0 or undefined.","Unit-test the config loader with empty strings, undefined, and Infinity."],"tags":["scheduler","configuration","validation","numeric"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}