Timeouts
Add deadlines to slow operations by racing them against a durable sleep.
A common requirement is bounding how long a workflow waits for something to finish — a slow step, an external webhook, a human approval. Race the operation against a durable sleep() with Promise.race() — whichever finishes first wins, and the loser keeps running but its result is ignored.
When to use this
- Slow steps — bound the time spent waiting on third-party APIs, model calls, or expensive computation
- External callbacks — give webhooks a deadline so the workflow doesn't hang forever waiting for an event that may never arrive
- Human approvals — auto-decline or escalate when a hook isn't resumed within a window
- Polling loops — give an outer poll-until-ready loop an overall budget
Pattern
Timeout on a slow step
import { sleep } from "workflow";
declare function processData(data: string): Promise<string>; // @setup
export async function processWithTimeout(data: string) {
"use workflow";
const result = await Promise.race([
processData(data),
sleep("30s").then(() => "timeout" as const),
]);
if (result === "timeout") {
throw new Error("Processing timed out after 30 seconds");
}
return result;
}Timeout on a webhook
The same pattern works for any promise — including hooks and webhooks. Here a webhook waits for an external service to call back, with a hard deadline of 7 days:
import { sleep, createWebhook } from "workflow";
declare function sendApprovalRequest(requestId: string, webhookUrl: string): Promise<void>; // @setup
export async function waitForApproval(requestId: string) {
"use workflow";
const webhook = createWebhook<{ approved: boolean }>();
await sendApprovalRequest(requestId, webhook.url);
const result = await Promise.race([
webhook.then((req) => req.json()),
sleep("7 days").then(() => ({ timedOut: true }) as const),
]);
if ("timedOut" in result) {
throw new Error("Approval request expired after 7 days");
}
// You may see warnings like `Workflow run completed with 1 uncommitted operations` in your
// logs when the workflow completes. This is expected behavior.
return result.approved;
}How it works
- Durable sleep —
sleep("30s")persists through restarts at zero compute cost. The workflow resumes precisely when the timer fires. - Race —
Promise.race([work, sleep(...)])returns the value of whichever promise resolves first. The loser keeps running in the background but its result is ignored by the workflow. - Discriminated result — tagging the sleep branch with a sentinel value (
"timeout" as const,{ timedOut: true }) lets TypeScript narrow the result and pick the right branch. - Throw to fail the workflow — inside a workflow function, throwing an
Errorexits the run with that error. UseFatalErrorinside steps; throw plain errors inside workflows.
The losing operation keeps running. Promise.race doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money. Use idempotency keys for non-idempotent side effects. For hard cancellation across processes, see Distributed Abort Controller, and see Idempotency for retry-safe side effects.
Adapting to your use case
- Different durations —
sleep()accepts duration strings ("30s","5m","7 days"), milliseconds, orDateobjects for absolute deadlines. - Soft timeout (retry) — instead of throwing, loop and retry with a fresh
Promise.raceand a backoff. - Soft timeout (fallback) — return a default value when the timer wins instead of throwing:
if (result === "timeout") return cachedFallback. - Combine with cancellation — race three promises: the operation, a deadline
sleep(), and a cancellation hook. See the Scheduling cookbook for the cancellation half of this pattern. - Per-step deadlines — wrap each step in its own
Promise.racefor independent budgets, or use a single outer race for an overall workflow deadline.
Key APIs
sleep()— durable wait (survives restarts, zero compute cost)createWebhook()— create a webhook URL the workflow can race againstdefineHook()— typed hook for in-process cancellation- Idempotency — protect side effects that may keep running after a timeout
Promise.race()— race operations against deadlines