anomalyco/sst · error · HeartbeatError

Failed to heartbeat workflow callback

Error message

Failed to heartbeat workflow callback

What it means

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.

Source

Thrown at sdk/js/src/aws/workflow.ts:491

   *
   * This is the equivalent to calling
   * [`SendDurableExecutionCallbackHeartbeat`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackHeartbeat.html).
   */
  export async function heartbeat(
    token: string,
    options?: Options,
  ): Promise<void> {
    const response = await awsFetch(
      "lambda",
      `/2025-12-01/durable-execution-callbacks/${encodeURIComponent(
        token,
      )}/heartbeat`,
      {
        method: "POST",
      },
      options,
    );
    if (!response.ok) throw new HeartbeatError(response);
  }

  export class StartError extends Error {
    constructor(public readonly response: Response) {
      super("Failed to start workflow");
    }
  }

  export class ListError extends Error {
    constructor(public readonly response: Response) {
      super("Failed to list workflows");
    }
  }

  export class DescribeError extends Error {
    constructor(public readonly response: Response) {
      super("Failed to describe workflow");
    }

View on GitHub (pinned to a0bd20f762)

Solutions

  1. 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).
  2. For 4xx (workflow completed/expired), stop heartbeating and treat the task as cancelled rather than retrying.
  3. For 5xx or transient network failures, retry the heartbeat with backoff before giving up.
  4. Verify the workflow's timeout is long enough for the task duration so the callback isn't invalidated mid-run.
  5. Ensure the callback URL comes from the current workflow invocation and is not cached or replayed.

Example fix

// before
await heartbeat(callback, options);

// after
try {
  await heartbeat(callback, options);
} catch (e) {
  if (e instanceof HeartbeatError && e.response.status >= 400 && e.response.status < 500) {
    // workflow no longer accepts heartbeats; abort work
    return;
  }
  // transient — retry with backoff
  await heartbeat(callback, options);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// heartbeat has no pre-check API; guard by checking workflow state before long work
if (!callback || typeof callback !== "string") throw new Error("No workflow callback to heartbeat");

Type guard

function isHeartbeatError(e: unknown): e is HeartbeatError {
  return e instanceof HeartbeatError && e.response instanceof Response;
}

Try / catch

try {
  await heartbeat(callback, options);
} catch (e) {
  if (isHeartbeatError(e)) {
    const retryable = e.response.status >= 500;
    if (!retryable) return; // workflow gone — stop
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30). Data as JSON: /api/errors/c405a79964e5282c. Report an issue: GitHub.