{"record":{"id":"2a864bd75c20f887","repo":"windmill-labs/windmill","slug":"giving-up-polling-job-jobid-after-max-consecu","errorCode":null,"errorMessage":"Giving up polling job ${jobId} after ${MAX_CONSECUTIVE_POLL_ERRORS} consecutive errors. Last error: ${err?.message ?? err}","messagePattern":"Giving up polling job (.+?) after (.+?) consecutive errors\\. Last error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/utils/job_polling.ts","lineNumber":117,"sourceCode":"        id: jobId,\n        getStarted: false,\n      });\n\n      consecutiveErrors = 0;\n\n      if (maybe.completed) {\n        return { result: maybe.result, success: maybe.success ?? false };\n      }\n    } catch (err: any) {\n      consecutiveErrors++;\n      log.warn(\n        colors.yellow(\n          `${label}${jobId}: error checking job status (${consecutiveErrors}/${MAX_CONSECUTIVE_POLL_ERRORS}): ${err?.message ?? err}`,\n        ),\n      );\n      lastHeartbeatAt = Date.now();\n      if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {\n        throw new Error(\n          `Giving up polling job ${jobId} after ${MAX_CONSECUTIVE_POLL_ERRORS} consecutive errors. Last error: ${err?.message ?? err}`,\n        );\n      }\n    }\n\n    if (Date.now() - lastQueueLogAt >= QUEUE_LOG_INTERVAL_MS) {\n      lastQueueLogAt = Date.now();\n      const logged = await logQueueStatus(workspace, jobId, label);\n      if (logged) lastHeartbeatAt = Date.now();\n    }\n\n    if (Date.now() - lastHeartbeatAt >= HEARTBEAT_INTERVAL_MS) {\n      lastHeartbeatAt = Date.now();\n      log.info(\n        colors.gray(\n          `${label}${jobId}: still polling, queue status unavailable...`,\n        ),\n      );","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/cli/src/utils/job_polling.ts#L99-L135","documentation":"pollJobWithQueueLogging in cli/src/utils/job_polling.ts polls getCompletedJobResultMaybe in a loop until a job finishes. Each failed status check increments a consecutive-error counter (successful polls reset it to 0); when MAX_CONSECUTIVE_POLL_ERRORS consecutive errors occur the loop throws this error containing the jobId and the last underlying error message. It signals that the CLI could not confirm the job's outcome, not that the job itself failed.","triggerScenarios":"Called by generateInlineScriptLock, updateFlow, pollForJobResult, fetchScriptLock etc.; thrown when MAX_CONSECUTIVE_POLL_ERRORS consecutive calls to wmill.getCompletedJobResultMaybe({workspace, id: jobId}) reject — e.g. connection resets, 5xx/429 responses, or workspace/token errors — in a row without a single successful poll in between.","commonSituations":"Backend restarted or briefly down during a long flow deploy; flaky VPN/network to a self-hosted instance; server overloaded returning 502/503; auth token expiring mid-poll; job deleted server-side so every poll 404s; polling through an unstable reverse proxy.","solutions":["Read 'Last error' in the message to identify the root cause (network vs HTTP status vs auth) and fix that first","Re-run the command once the backend is reachable — the job may have completed server-side; check its result in the UI or via `wmill job result <jobId>`","Verify connectivity: curl the instance's /api endpoint from the same machine","Re-login (wmill login) if the token expired mid-run","For persistent self-hosted flakiness, address the upstream instability (proxy timeouts, worker restarts) rather than retrying the CLI"],"exampleFix":"// before: transient backend restart aborts the command\n// Giving up polling job X after N consecutive errors. Last error: fetch failed\n\n// after: catch and re-check the job result before retrying the whole command\ntry {\n  await pollForJobResult(workspace, jobId);\n} catch (e) {\n  const maybe = await wmill.getCompletedJobResultMaybe({ workspace, id: jobId });\n  if (!maybe.completed) throw e; // job genuinely unresolved; retry later\n}","handlingStrategy":"retry","validationCode":"// before starting a long operation, confirm the instance is reachable and accepting API calls\nconst ping = await fetch(`${process.env.BASE_URL}/api/workspaces/list`, { headers: authHeaders });\nif (!ping.ok) throw new Error(`Windmill API unhealthy (HTTP ${ping.status}); polling would exhaust retries`);","typeGuard":"function isPollGiveUpError(err: unknown): err is Error & { jobId?: string } {\n  return err instanceof Error && /^Giving up polling job /.test(err.message);\n}","tryCatchPattern":"try {\n  await pollForJobResult(workspace, jobId);\n} catch (e) {\n  if (isPollGiveUpError(e)) {\n    // jobId is in the message; check the job out-of-band before retrying\n    const id = e.message.match(/job ([^ ]+) /)?.[1];\n    const maybe = await wmill.getCompletedJobResultMaybe({ workspace, id });\n    if (!maybe.completed) await sleep(5000); // then retry the poll\n  } else throw e;\n}","preventionTips":["Monitor backend uptime/proxy health for the instance the CLI polls","Keep auth tokens fresh for long-running flows (token expiry mid-poll exhausts retries)","Avoid polling through aggressive reverse proxies with short idle timeouts","Treat 'Last error' in the message as the root cause and fix connectivity/auth, not the polling loop","Check job status in the UI after a give-up — the job often completed server-side"],"tags":["network","polling","timeout","cli","reliability"],"backgroundTag":"poll-exhausted-retries","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}