- Durable execution with fibers·续
- Checkpoints with stash
- Recovery
- Concurrent fibers
- Testing locally
- API reference
- runFiber(name, fn)
- startFiber(name, fn, options)
- inspectFiber(fiberId) / inspectFiberByKey(idempotencyKey)
- listFibers(options)
- cancelFiber(fiberId, reason) / cancelFiberByKey(idempotencyKey, reason)
- resolveFiber(fiberId, result)
- deleteFibers(options)
- stash(data) / ctx.stash(data)
- onFiberRecovered(ctx)
- keepAlive()
- keepAliveWhile(fn)
- Related
- On this page
Durable execution with fibers·续
If recovery is triggered by a later duplicate webhook instead of onFiberRecovered(), use resolveFiber() with the same result shape after your application-level recovery succeeds. resolveFiber() only updates managed fibers that are currently interrupted; it returns false for pending, running, or already-terminal rows.
Checkpoints with stash
ctx.stash(data) writes to SQLite synchronously. There is no async gap between "I decided to save" and "it is saved." If eviction happens after stash() returns, the data is guaranteed to be in SQLite.
Each call fully replaces the previous snapshot — it is not a merge. Write the complete recovery state you need:
await this.runFiber("research", async (ctx) => {
const steps = ["search", "analyze", "synthesize"];
const completed: string[] = [];
const results: Record<string, unknown> = {};
for (const step of steps) {
results[step] = await executeStep(step);
completed.push(step);
ctx.stash({
completed,
results,
pendingSteps: steps.slice(completed.length),
});
}
});
this.stash vs ctx.stash
Both do the same thing. ctx.stash() uses a direct closure over the fiber ID. this.stash() uses AsyncLocalStorage to find the currently executing fiber — it works correctly even with concurrent fibers, since each fiber's ALS context is independent.
this.stash() is convenient when calling from nested functions that do not have access to ctx. It throws if called outside a runFiber callback.
Recovery
Override onFiberRecovered to handle interrupted fibers. The default implementation logs a warning and deletes the row.
class ResearchAgent extends Agent {
async onFiberRecovered(ctx: FiberRecoveryContext) {
if (ctx.name !== "research") return;
const snapshot = ctx.snapshot as {
completed: string[];
results: Record<string, unknown>;
pendingSteps: string[];
} | null;
if (snapshot && snapshot.pendingSteps.length > 0) {
void this.runFiber("research", async (fiberCtx) => {
const { completed, results, pendingSteps } = snapshot;
for (const step of pendingSteps) {
results[step] = await this.executeStep(step);
completed.push(step);
fiberCtx.stash({
completed,
results,
pendingSteps: pendingSteps.slice(pendingSteps.indexOf(step) + 1),
});
}
});
}
}
}
Key points:
- The original lambda is gone. On recovery, you only have the
nameandsnapshot. The lambda cannot be serialized — recovery logic must be in the hook. - Unmanaged
runFiber()rows are deleted after the hook returns successfully. If you want to continue unmanaged work, callrunFiber()again inside the hook — this creates a new row. - Managed
startFiber()rows are retained. Return aFiberRecoveryResultto mark an interrupted managed fiber ascompleted,error,aborted, or stillinterrupted. - You control what recovery means. Retry from the beginning, resume from a checkpoint, skip and notify the user, or do nothing. The framework does not impose a strategy.
- If the hook throws, the row is kept (up to a bound). A later startup or alarm scan retries recovery, which protects against transient storage or scheduling failures. Catch application-level errors yourself when you want to mark the work terminal instead of retrying. A hook that always throws is retried on a backing-off schedule (the recovery alarm uses an exponential delay capped at 5 minutes, so it is not a busy-loop) until the row exceeds
fiberRecoveryMaxAgeMs(default 24 h), after which it is discarded with afiber:recovery:skipped(reason: "max_age_exceeded") event. SettingfiberRecoveryMaxAgeMs: 0retains such rows indefinitely — recovery keeps retrying on the capped backoff, and the Durable Object never idle-evicts while an un-recoverable row exists, so prefer a finite age unless you intend to inspect or clear those rows yourself. For managed work, the retained row staysinterruptedand records the recovery error for inspection.
Chat recovery
AIChatAgent and Think build on fibers for LLM streaming recovery. Every chat turn is wrapped in a fiber automatically. The framework handles the internal recovery path and exposes onChatRecovery for provider-specific strategies. Refer to Long-running agents: Recovering interrupted LLM streams for details.
Concurrent fibers
Multiple fibers can run at the same time. Each has its own row in SQLite with its own snapshot, and each calls keepAlive() independently (ref-counted, so the DO stays alive until all fibers complete).
void this.runFiber("fetch-data", async (ctx) => {
/* ... */
});
void this.runFiber("process-queue", async (ctx) => {
/* ... */
});
On recovery, all orphaned rows are iterated and onFiberRecovered is called for each. Use ctx.name to distinguish between fiber types in your recovery hook.
Testing locally
In wrangler dev, fiber recovery works identically to production. SQLite and alarm state persist to disk between restarts.
- Start your agent and trigger a fiber (
runFiber) - Kill the wrangler process (Ctrl-C or SIGKILL)
- Restart wrangler
- Recovery fires automatically — via
onStart()if a request arrives, or via the persisted alarm if no clients connect
API reference
runFiber(name, fn)
Execute a durable fiber. The fiber is registered in SQLite before fn runs and deleted after it completes (or throws). keepAlive() is held for the duration.
name— identifier for the fiber, used inonFiberRecoveredto distinguish fiber types. Not unique — multiple fibers can share a name.fn— async function receiving aFiberContext. Closures work naturally (thisand local variables are captured).- Returns — the value returned by
fn. If the DO is evicted before completion, the return value is lost; recovery happens through the hook.
startFiber(name, fn, options)
Durably accept a retained background fiber. The returned StartFiberResult includes a generated fiberId, current status, optional metadata, and accepted, which is false when an existing fiber matched the same idempotency key.
name— identifier for the managed fiber, used in inspection and recovery.fn— async function receiving aFiberContext. The function result is not stored.options.idempotencyKey— stable external key used to dedupe retries.options.metadata— JSON-serializable data stored with the retained row.options.waitForCompletion— wait for terminal status before returning.
inspectFiber(fiberId) / inspectFiberByKey(idempotencyKey)
Return the retained status row for a managed fiber, or null if no row exists.
listFibers(options)
List retained managed fibers. Filter by status or name, and use limit to cap the result set.
cancelFiber(fiberId, reason) / cancelFiberByKey(idempotencyKey, reason)
Mark a managed fiber as aborted and abort its in-memory ctx.signal when it is running in the current isolate. Returns false if the fiber does not exist or is already terminal.
resolveFiber(fiberId, result)
Resolve an interrupted managed fiber after application-level recovery succeeds. Returns false for pending, running, or already-terminal rows.
deleteFibers(options)
Delete retained managed fiber rows. By default, settled completed, error, and aborted rows are eligible. Pass status, settledBefore, or limit to narrow cleanup.
stash(data) / ctx.stash(data)
Checkpoint the current fiber's state. Writes synchronously to SQLite. Each call fully replaces the previous snapshot. data must be JSON-serializable.
onFiberRecovered(ctx)
Called once per orphaned fiber row on agent restart. Override to implement recovery. Unmanaged runFiber() rows are deleted after this hook returns successfully; if recovery throws, the row is left for a later scan so transient failures do not lose the recovery handle. Managed startFiber() rows stay retained and can be resolved by returning a FiberRecoveryResult.
ctx.id— unique fiber IDctx.name— the name passed torunFiber()ctx.status— retained status for managed fibersctx.idempotencyKey— idempotency key for managed fibers, if suppliedctx.metadata— metadata for managed fibers, if suppliedctx.snapshot— the laststash()data, ornullifstash()was never calledctx.createdAt— epoch milliseconds whenrunFiber()started. Compare againstDate.now()to skip recoveries that are too old to replay safely.ctx.recoveryReason— why recovery is running. Currently always"interrupted"for eviction or restart recovery.
keepAlive()
Create a 30-second alarm heartbeat. Returns a disposer function. Idempotent — calling the disposer multiple times is safe.
keepAliveWhile(fn)
Run an async function while keeping the DO alive. Heartbeat starts before fn and stops when it completes or throws. Returns the value returned by fn.
Related
- Long-running agents — how fibers compose with schedules, plans, and async operations
- Schedule tasks —
keepAlivedetails and the alarm system - Sub-agents — durable execution and schedules inside sub-agents
- Workflows — durable multi-step execution outside the agent
- Chat agents —
chatRecoveryandonChatRecovery
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/execution/durable-execution/#page","headline":"Durable execution with fibers","description":"Run work that survives Durable Object eviction with runFiber(), startFiber(), keepAlive(), and crash recovery.","url":"https://developers.cloudflare.com/agents/runtime/execution/durable-execution/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/execution/durable-execution/og.png?v=a2889c46d9e8ecae","dateModified":"2026-08-20","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
description: Add background tasks to a built-in FIFO queue for asynchronous processing within Cloudflare Agents. title: Queue tasks image: https://developers.cloudflare.com/agents/runtime/execution/queue-tasks/og.png?v=63ff62770e95a3dd
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs