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
- 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.
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
- 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.
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
- bad status: %s
- The VPC configured for the service does not have NAT enabled
- Set "vpc.publicSubnets" on the Cluster to use "public" on th
- Failed to publish event to bus
- Failed to describe task
AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30).
Data as JSON: /api/errors/c405a79964e5282c.
Report an issue: GitHub.