{"record":{"id":"fbf3440cfb58fac6","repo":"cockroachdb/cockroach","slug":"unable-to-retrieve-the-jobs-table","errorCode":null,"errorMessage":"Unable to retrieve the Jobs table.","messagePattern":"Unable to retrieve the Jobs table\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pkg/ui/workspaces/db-console/src/util/api.ts","lineNumber":415,"sourceCode":"export const jobsTimeoutErrorMessage = \"Unable to retrieve the Jobs table.\";\n\nexport function getJobs(\n  req: JobsRequestMessage,\n  timeout?: moment.Duration,\n): Promise<JobsResponseMessage> {\n  const url = `${API_PREFIX}/jobs?status=${req.status}&type=${req.type}&limit=${req.limit}`;\n  return timeoutFetch(serverpb.JobsResponse, url, null, timeout).then(\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    (response: JobsResponseMessage) => response,\n    (err: Error) => {\n      if (err instanceof TimeoutError) {\n        // eslint-disable-next-line no-console\n        console.error(\n          `Jobs page time out because attempt to retrieve jobs exceeded ${err.timeout.asMilliseconds()}ms.`,\n          `URL: ${url}. Request: ${JSON.stringify(req)}`,\n        );\n        throw new Error(jobsTimeoutErrorMessage);\n      } else {\n        throw err;\n      }\n    },\n  );\n}\n\nexport function getJob(\n  req: JobRequestMessage,\n  timeout?: moment.Duration,\n): Promise<JobResponseMessage> {\n  return timeoutFetch(\n    serverpb.JobResponse,\n    `${API_PREFIX}/jobs/${req.job_id}`,\n    null,\n    timeout,\n  );\n}","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/cockroachdb/cockroach/blob/8812064a015d2faf99d3fc7e15880f94042954b0/pkg/ui/workspaces/db-console/src/util/api.ts#L397-L433","documentation":"getJobs in db-console wraps timeoutFetch of `_admin/jobs?status=&type=&limit=`; when the fetch exceeds its deadline, the TimeoutError is converted to this fixed message (jobsTimeoutErrorMessage = 'Unable to retrieve the Jobs table.') while the URL and request are console.error'd. Non-timeout errors propagate unchanged, so seeing this exact string means the admin jobs RPC was too slow, not failed.","triggerScenarios":"GET /_admin/jobs not answering within the timeout: the jobs registry mutex held by a long operation on a node, very high limit combined with thousands of jobs, slow RPC fan-out across many nodes, or an overloaded node.","commonSituations":"Large clusters with heavy backup/restore/changefeed job churn; opening the Jobs page unfiltered with a big limit during node stress; transient GC pauses or disk stalls on a node making job enumeration slow.","solutions":["Retry the page once after a few seconds — registry contention is often transient","Narrow the request: filter by status and type and lower limit before loading","If persistent, capture a goroutine dump (/_status/stacks or debug zip) to find what holds the jobs registry mutex","As a last resort raise the timeout passed to getJobs for this page on known-slow clusters"],"exampleFix":"// before\nconst [jobs, setJobs] = useState(null);\ngetJobs(req).then(setJobs);\n\n// after: one bounded retry for the timeout-specific message\nimport { jobsTimeoutErrorMessage } from 'src/util/api';\nconst load = (attempt: number) =>\n  getJobs(req).catch(err => {\n    if (err.message === jobsTimeoutErrorMessage && attempt < 1) {\n      return new Promise(r => setTimeout(r, 3000)).then(() => load(attempt + 1));\n    }\n    throw err;\n  });","handlingStrategy":"retry","validationCode":"// Keep requests bounded before loading\nconst bounded = {\n  ...req,\n  limit: Math.min(req.limit, 100),\n  status: req.status || 'running',\n  type: req.type || '',\n};","typeGuard":"const isJobsTimeoutError = (e: unknown): e is Error =>\n  e instanceof Error && e.message === 'Unable to retrieve the Jobs table.';","tryCatchPattern":"import { jobsTimeoutErrorMessage } from 'src/util/api';\nconst loadJobs = async (attempt = 0): Promise<JobsResponseMessage> => {\n  try {\n    return await getJobs(req);\n  } catch (e) {\n    if (e.message === jobsTimeoutErrorMessage && attempt < 2) {\n      await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n      return loadJobs(attempt + 1);\n    }\n    throw e;\n  }\n};","preventionTips":["Always filter by status/type and use a small limit on large clusters","Retry with backoff on this specific message — it signals slowness, not permanent failure","If timeouts persist, capture goroutine dumps to identify registry-mutex contention"],"tags":["db-console","jobs","timeout","network","typescript"],"backgroundTag":null,"analyzedSha":"8812064a015d2faf99d3fc7e15880f94042954b0","analyzedAt":"2026-08-15T16:34:17.351Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}