{"record":{"id":"689c13cad640a0a6","repo":"tinyhumansai/openhuman","slug":"core-rpc-payload-method-timed-out-after-effec","errorCode":null,"errorMessage":"Core RPC ${payload.method} timed out after ${effectiveTimeoutMs}ms","messagePattern":"Core RPC (.+?) timed out after (.+?)ms","errorType":"exception","errorClass":"CoreRpcError","httpStatus":null,"severity":"error","filePath":"app/src/services/coreRpcClient.ts","lineNumber":726,"sourceCode":"          JSON.stringify(payload),\n          controller.signal\n        );\n      } else {\n        response = await fetch(rpcUrl, {\n          method: 'POST',\n          headers,\n          body: JSON.stringify(payload),\n          signal: controller.signal,\n        });\n      }\n    } catch (fetchErr) {\n      if (controller.signal.aborted) {\n        // Throw a fully-classified `CoreRpcError` here so the outer catch\n        // doesn't re-wrap a bare `Error` and so callers can branch on\n        // `err.kind === 'timeout'` (Sentry filter, soft toast skip). Use\n        // the per-call `effectiveTimeoutMs` so the message reflects the\n        // actual budget (#2156 raised the snapshot path to 90s).\n        throw new CoreRpcError(\n          `Core RPC ${payload.method} timed out after ${effectiveTimeoutMs}ms`,\n          'timeout'\n        );\n      }\n      throw fetchErr;\n    } finally {\n      clearTimeout(timeoutId);\n    }\n\n    if (!response.ok) {\n      const text = await response.text();\n      const httpMessage = `Core RPC HTTP ${response.status}: ${text || response.statusText}`;\n      const kind = classifyRpcError(text || response.statusText, response.status);\n      if (kind === 'auth_expired' && !suppressAuthExpiredEvent)\n        dispatchAuthExpired(\n          payload.method,\n          classifyAuthExpiredReason(text || response.statusText, response.status)\n        );","sourceCodeStart":708,"sourceCodeEnd":744,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/coreRpcClient.ts#L708-L744","documentation":"A CoreRpcError with kind 'timeout' thrown by callCoreRpc: every RPC is bounded by an AbortController + setTimeout (manually wired so fake timers work in tests), with a per-call timeoutMs clamped into effectiveTimeoutMs. When the fetch aborts before the core answers, the abort is detected in the catch and re-thrown as this classified error so callers and Sentry filters can branch on err.kind === 'timeout'.","triggerScenarios":"The core is slow or hung: first-launch app_state_snapshot on a cold workspace (this path was explicitly raised to 90s in #2156), long config migrations at boot, a deadlocked or CPU-pinned core, or a genuinely slow method called with the default budget.","commonSituations":"Cold start with a large workspace/migration; core stuck on a lock (store contention); heavy operations (memory indexing) starving the RPC thread pool; slower hardware stretching slow calls past the default budget.","solutions":["For known-slow methods, pass a larger per-call budget: callCoreRpc({ method, params, timeoutMs: 90_000 })","Retry once — transient core congestion often clears; treat kind === 'timeout' as retryable (the analytics module already special-cases it)","Restart the core if it is consistently timing out (possible deadlock) and check core CPU / logs around the timeouts","If you are adding a legitimately slow RPC, budget its timeoutMs like #2156 did rather than letting it hit the default"],"exampleFix":"// before\nawait callCoreRpc({ method: 'openhuman.app_state_snapshot' });\n\n// after\nawait callCoreRpc({ method: 'openhuman.app_state_snapshot', timeoutMs: 90_000 });","handlingStrategy":"retry","validationCode":"// Budget slow methods explicitly instead of relying on the default\nawait callCoreRpc({ method: 'openhuman.app_state_snapshot', timeoutMs: 90_000 });","typeGuard":"import { CoreRpcError } from '../services/coreRpcClient';\nfunction isRpcTimeout(e: unknown): e is CoreRpcError {\n  return e instanceof CoreRpcError && e.kind === 'timeout';\n}","tryCatchPattern":"try {\n  return await callCoreRpc(payload);\n} catch (e) {\n  if (isRpcTimeout(e)) {\n    await delay(1000);\n    return callCoreRpc(payload); // one bounded retry; report if it repeats\n  }\n  throw e;\n}","preventionTips":["Pass a generous timeoutMs for cold-start and known-slow methods (see #2156)","Branch on err.kind === 'timeout' — Sentry/toast filters already special-case it","Consistent timeouts across methods usually mean a wedged core: restart rather than retry forever"],"tags":["rpc","timeout","performance","abort-controller"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}