{"record":{"id":"c405a79964e5282c","repo":"anomalyco/sst","slug":"failed-to-heartbeat-workflow-callback","errorCode":null,"errorMessage":"Failed to heartbeat workflow callback","messagePattern":"Failed to heartbeat workflow callback","errorType":"error_code","errorClass":"HeartbeatError","httpStatus":null,"severity":"error","filePath":"sdk/js/src/aws/workflow.ts","lineNumber":491,"sourceCode":"   *\n   * This is the equivalent to calling\n   * [`SendDurableExecutionCallbackHeartbeat`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackHeartbeat.html).\n   */\n  export async function heartbeat(\n    token: string,\n    options?: Options,\n  ): Promise<void> {\n    const response = await awsFetch(\n      \"lambda\",\n      `/2025-12-01/durable-execution-callbacks/${encodeURIComponent(\n        token,\n      )}/heartbeat`,\n      {\n        method: \"POST\",\n      },\n      options,\n    );\n    if (!response.ok) throw new HeartbeatError(response);\n  }\n\n  export class StartError extends Error {\n    constructor(public readonly response: Response) {\n      super(\"Failed to start workflow\");\n    }\n  }\n\n  export class ListError extends Error {\n    constructor(public readonly response: Response) {\n      super(\"Failed to list workflows\");\n    }\n  }\n\n  export class DescribeError extends Error {\n    constructor(public readonly response: Response) {\n      super(\"Failed to describe workflow\");\n    }","sourceCodeStart":473,"sourceCodeEnd":509,"githubUrl":"https://github.com/anomalyco/sst/blob/a0bd20f762883e72a35caccb4896c42ce5b3f707/sdk/js/src/aws/workflow.ts#L473-L509","documentation":"SST's workflow SDK throws HeartbeatError when the POST to the workflow callback's /heartbeat endpoint returns a non-ok HTTP response. Heartbeats tell the workflow engine the callback/task is still alive so it does not time out. Any 4xx/5xx from the heartbeat endpoint — expired callback, revoked workflow, or infrastructure error — produces this error.","triggerScenarios":"Calling heartbeat() on a WorkflowCallback whose POST to `${callback}/heartbeat` returns response.ok === false — e.g. the workflow already completed/timed out, the callback URL/token is expired or invalid, or the workflow service returned a server error.","commonSituations":"Long-running tasks whose heartbeat outlives the workflow's configured timeout; replaying or reusing a saved callback after the workflow ended; transient network/API errors; running outside the workflow runtime with a stale or fabricated callback URL.","solutions":["Inspect the response property on the caught HeartbeatError for the exact HTTP status and body to determine whether the workflow is gone (4xx) or the service failed (5xx).","For 4xx (workflow completed/expired), stop heartbeating and treat the task as cancelled rather than retrying.","For 5xx or transient network failures, retry the heartbeat with backoff before giving up.","Verify the workflow's timeout is long enough for the task duration so the callback isn't invalidated mid-run.","Ensure the callback URL comes from the current workflow invocation and is not cached or replayed."],"exampleFix":"// before\nawait heartbeat(callback, options);\n\n// after\ntry {\n  await heartbeat(callback, options);\n} catch (e) {\n  if (e instanceof HeartbeatError && e.response.status >= 400 && e.response.status < 500) {\n    // workflow no longer accepts heartbeats; abort work\n    return;\n  }\n  // transient — retry with backoff\n  await heartbeat(callback, options);\n}","handlingStrategy":"try-catch","validationCode":"// heartbeat has no pre-check API; guard by checking workflow state before long work\nif (!callback || typeof callback !== \"string\") throw new Error(\"No workflow callback to heartbeat\");","typeGuard":"function isHeartbeatError(e: unknown): e is HeartbeatError {\n  return e instanceof HeartbeatError && e.response instanceof Response;\n}","tryCatchPattern":"try {\n  await heartbeat(callback, options);\n} catch (e) {\n  if (isHeartbeatError(e)) {\n    const retryable = e.response.status >= 500;\n    if (!retryable) return; // workflow gone — stop\n  }\n  throw e;\n}","preventionTips":["Set workflow timeouts comfortably longer than the task's worst-case duration.","Catch HeartbeatError separately from other errors and branch on response.status.","Never reuse a callback URL from a previous invocation.","Use exponential backoff for 5xx/network heartbeat failures."],"tags":["network","http","workflow","heartbeat"],"backgroundTag":"http-non-ok-response","analyzedSha":"a0bd20f762883e72a35caccb4896c42ce5b3f707","analyzedAt":"2026-08-30T11:26:00.383Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}