{"record":{"id":"483512c30ef3287b","repo":"thedotmack/claude-mem","slug":"opts-label-request-exceeded-the-opts-perattempttimeoutms-ms","errorCode":null,"errorMessage":"${opts.label ?? 'Request'} exceeded the ${opts.perAttemptTimeoutMs}ms per-attempt deadline. Raise CLAUDE_MEM_LLM_TIMEOUT_MS if the backend is simply slow.","messagePattern":"(.+?) exceeded the (.+?)ms per-attempt deadline\\. Raise CLAUDE_MEM_LLM_TIMEOUT_MS if the backend is simply slow\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/worker/retry.ts","lineNumber":146,"sourceCode":"    const attemptController = new AbortController();\n    let deadlineExpired = false;\n    const timeoutHandle = setTimeout(() => {\n      deadlineExpired = true;\n      attemptController.abort();\n    }, opts.perAttemptTimeoutMs);\n    const onExternalAbort = () => attemptController.abort();\n    options.abortSignal?.addEventListener('abort', onExternalAbort, { once: true });\n\n    try {\n      return await fn(attemptController.signal);\n    } catch (err: unknown) {\n      lastError = err;\n\n      // Our own deadline, not a network blip. The abort surfaces with no HTTP  // status, so it classifies as transient and was retried twice — against a\n      // backend that is already saturated, those attempts are what turn a\n      // latency problem into a congestion collapse. Raise the deadline instead.\n      if (deadlineExpired) {\n        throw new Error(\n          `${opts.label ?? 'Request'} exceeded the ${opts.perAttemptTimeoutMs}ms per-attempt deadline. `\n          + 'Raise CLAUDE_MEM_LLM_TIMEOUT_MS if the backend is simply slow.',\n          { cause: err },\n        );\n      }\n\n      if (!isRetryableKind(err)) {\n        throw err;\n      }\n    \n\n      if (attempt === opts.maxRetries) {\n        throw err;\n      }\n\n      // Honor retryAfterMs from rate_limit errors; otherwise exponential backoff.\n      let delayMs: number;\n      if (isClassified(err) && err.kind === 'rate_limit' && err.retryAfterMs !== undefined) {","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/worker/retry.ts#L128-L164","documentation":"withRetry's own per-attempt deadline (perAttemptTimeoutMs, configurable via CLAUDE_MEM_LLM_TIMEOUT_MS) fired and the abort was about to be misclassified as a transient network error and retried. The library deliberately converts it into a non-retryable error: retrying against an already-slow backend turns latency into congestion collapse, so it tells you to raise the deadline instead.","triggerScenarios":"Any LLM call wrapped in withRetry (summarization, memory compression) whose single attempt exceeds perAttemptTimeoutMs — the abort error surfaces with no HTTP status, so it would otherwise look transient.","commonSituations":"Large summarization prompts on an overloaded or rate-limited backend; CLAUDE_MEM_LLM_TIMEOUT_MS left at its default for a slow local/proxy LLM; network path with high latency to the backend; model cold-start latency.","solutions":["Raise CLAUDE_MEM_LLM_TIMEOUT_MS (e.g. from default to 120000–300000) and retry the operation","Check backend health/load — if the backend is saturated, fix capacity rather than the timeout","Reduce prompt size or batch size so attempts fit within the deadline","If the error persists immediately at the same threshold, confirm CLAUDE_MEM_LLM_TIMEOUT_MS is actually reaching the worker process (env not stripped by your service manager)"],"exampleFix":"// before\n// worker started without timeout env; slow backend hits the 60s default\n// after\n// in the worker's environment:\nCLAUDE_MEM_LLM_TIMEOUT_MS=180000","handlingStrategy":"retry","validationCode":"// before wrapping the call, ensure the env knob is set appropriately:\nconst timeoutMs = Number(process.env.CLAUDE_MEM_LLM_TIMEOUT_MS ?? 60000);\nif (!(timeoutMs > 0)) throw new Error('CLAUDE_MEM_LLM_TIMEOUT_MS must be a positive number');","typeGuard":null,"tryCatchPattern":"try {\n  return await withRetry(() => callLlm(prompt), { label: 'Summarize', perAttemptTimeoutMs });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('per-attempt deadline')) {\n    // non-retryable by design: raise CLAUDE_MEM_LLM_TIMEOUT_MS or shrink the prompt\n    logger.error('LLM attempt exceeded deadline — not retrying to avoid congestion collapse');\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Set CLAUDE_MEM_LLM_TIMEOUT_MS generously (2–5 min) for large summarization workloads","Confirm the env var actually reaches the worker process (service managers often strip env)","Watch backend latency/queue depth and scale capacity before deadlines fire","Keep prompts/batches small enough that a single attempt fits the deadline with headroom"],"tags":["timeout","retry","llm"],"backgroundTag":"request-timeout","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}