Durable Functions

Workflows that pick up where they left off

Write the whole workflow as one function. Volcano checkpoints every step, so after a crash or a month-long wait it resumes from the last finished step instead of starting over.

Durable Functions product screenshot
durable
volcano cloud durable deploy --all[1/1] Deploying order-pipeline...  Runtime: nodejs24.x✓ 1/1 durable function(s) deployment startedvolcano cloud durable start order-pipeline --input '{"order_id":4417}' --name order-4417✓ Execution order-4417 startedvolcano cloud durable executions get order-pipeline 9a8b7c6d-…Status  succeeded   Duration  16m 53s# fifteen of those minutes were a wait, and a wait holds no runtime
Overview

Long work fails in the middle

Anything with more than one step eventually fails between two of them. The card was charged but the confirmation never went out, the import stopped at row 40,000, the approval arrived after the request waiting for it had timed out. The usual fix is a queue, a state table, a retry policy and a worker that knows where the last one died — a second system to build before the feature ships.

A durable function is that system, written as ordinary code. Wrap the handler in durable() and do the work through ctx.step. Volcano records each result as it completes, so when an attempt crashes or the function suspends on a wait, the next run replays what already finished and carries on from the first step that did not. Waits are held by the platform, so an execution can sit for a week without keeping anything running.

Checkpoints

Finished steps never run again

ctx.step runs a piece of work and stores what it returned. When the execution resumes — after a crash, a retry or a wait — finished steps hand back their recorded result instead of running again. For the call that must never repeat, like a charge, atMostOnce with retry: false records the step before it runs.

  • Failed steps retry with exponential backoff
  • atMostOnce with retry: false for a charge or an email
  • ctx.map fans out with a concurrency cap
order-4417, resumed after a failed attempt
step  load-order         succeeded      84 msstep  charge             succeeded     412 mswait  settle             15m           nothing runningmap   pack · 12 items    succeeded     concurrency 5step  dispatch           failed        retrying, attempt 2✓ resumed at dispatch · the charge was not repeated
Time

Wait a month without running anything

ctx.wait suspends the execution and the platform keeps the timer, so a 30-day grace period is one line rather than a cron job and a state column. ctx.waitUntil checks your own state on an interval that backs off, which is how a workflow waits for a person to approve something or for a third-party job to land.

  • Waits from one second up to the execution's full year
  • No runtime held, and no compute charged, while suspended
  • ctx.waitUntil for approvals and external jobs
waiting on a person
await ctx.wait('grace-period', '30d');const review = await ctx.waitUntil('await-review', readReview, {  initialState: 'pending', until: (s) => s !== 'pending', interval: '1m' });# suspended between checks — nothing is runningapproved after 52m · resumed at the next step
Operations

Every execution, where you can see it

Executions are listed per function with their status, region and duration, from the dashboard, the CLI or the SDK. Start one by hand, read its result, or stop it — stopping never undoes a step that already finished.

  • Every status, from running to succeeded, failed or stopped
  • Results kept for 30 days after an execution ends
  • Build and runtime logs per function, live with --follow
volcano.dev/dashboard/durable-functions
A durable function's executions with their status, region and duration
Capabilities

What every durable function gets

JavaScript and Python

The same step, wait, waitUntil, map, parallel and child operations in both, on Node.js 22 and 24 or Python 3.13 and 3.14.

Up to a year per execution

One execution can run for 366 days across as many resumes as it needs. Each step gets up to 300 seconds on Hobby or 900 seconds on Superagent.

Retries built in

A failed step retries with exponential backoff. Set the attempts, the delays and which errors count, or turn retries off for that step.

Idempotent starts

The execution name is the idempotency key, so a retried start returns the execution that already exists and is charged once.

Cron schedules

Attach a cron expression. Each tick starts an execution named after the tick, so a retried tick never does the work twice.

Metered on what runs

Executions, operations and GB-seconds have their own monthly allowances on both plans. Time spent waiting is not charged.

Code

Write it, start it, run it

JavaScript
const { durable } = require('@volcano.dev/sdk/durable'); exports.handler = durable(async (input, ctx) => { const order = await ctx.step('load-order', () => loadOrder(input.order_id)); // Checkpointed before it runs and never retried, so an attempt that dies // mid-charge cannot bill the customer twice. const charge = await ctx.step('charge', () => chargeCard(order), { atMostOnce: true, retry: false, }); // Let the charge settle. Nothing runs while the execution waits. await ctx.wait('settle', '15m'); const packed = await ctx.map( 'pack', order.items, (item, itemCtx) => itemCtx.step('pack-item', () => packItem(item)), { concurrency: 5 }, ); packed.throwIfFailed(); // Large orders wait for a person. Nothing runs between checks, and 200 of // them, backing off from a minute to half an hour, last about four days. if (order.total_cents > 100_000) { const review = await ctx.waitUntil('await-review', () => reviewStatus(order.id), { initialState: 'pending', until: (status) => status !== 'pending', interval: '1m', maxInterval: '30m', maxAttempts: 200, }); if (review === 'rejected') { await ctx.step('refund', () => refund(charge.id), { atMostOnce: true, retry: false }); return { order_id: order.id, outcome: 'refunded' }; } } await ctx.step('dispatch', () => dispatch(order, packed.results)); return { order_id: order.id, outcome: 'shipped' }; });
Use cases

What people run as durable functions

one order, four days
mon 09:12  charge        succeededmon 09:12  settle        waited 15mmon 09:27  pack · 3      succeededmon 09:27  await-review  waiting, checks back off to 30mthu 16:40  await-review  approvedthu 16:40  dispatch      succeeded · outcome shipped
Platform

Works with the rest of Volcano

Functions and agents
  • Call your other functions

    Invoke a standard function from inside a step, so a workflow reuses the endpoints you already deployed.

Databases and vector
  • State the workflow reads

    An approval, a job status or a row to process lives in Postgres, where ctx.waitUntil checks it and your app writes it.

Authentication
  • Started by a signed-in user

    A start made with the user's session reaches the handler with their verified identity on the input, never one the client wrote.

Realtime
  • Progress on the page

    Broadcast from a step as the workflow advances, so the page shows where it is without polling the execution.

Frequently asked questions

Read the docs
How is a durable function different from a standard function?

A standard function answers one request within its timeout. A durable function is started rather than invoked: it returns an execution handle straight away, keeps working in the background for up to 366 days, and checkpoints each step so it resumes instead of restarting. The two are separate resources, and a function's kind is fixed when it is created.

Which languages can I write one in?

JavaScript on Node.js 22 or 24, and Python on 3.13 or 3.14, with the same operations in both. Ruby apps can start and read executions through the Ruby SDK, but the handler itself is written in JavaScript or Python.

What does a long wait cost?

Nothing while it waits. Compute is charged only for the time your code is actually running, and a suspended execution holds no runtime. The wait itself counts as one operation, as does each step attempt and each waitUntil check, and an execution can use up to 3,000 operations. What a waiting execution does hold is one of your project's concurrent-execution slots, until it ends.

Can a workflow wait for a human approval?

Yes, with ctx.waitUntil. Your approval endpoint writes the decision somewhere the check can read it, and the execution sleeps between checks on an interval that backs off. Volcano has no callback API that completes a waiting execution from outside, so the approval is read from your own state rather than pushed in.

How are durable functions billed?

On executions, operations and GB-seconds of compute, each with its own monthly allowance and metered separately from function requests. Hobby projects can run up to 10 executions at once with 128 MB each; Superagent projects can run up to 100 with 1 GB each. The full figures are on the pricing page.

Can I run them on my machine?

Yes. volcano start runs durable functions locally with the same handler and the same volcano-config.yaml, so what works locally deploys unchanged. Waits resolve immediately so a day-long wait takes seconds, and LOCAL_DURABLE_REAL_TIME=true keeps real timing when you need it.

Ready to run your first workflow?

Build, deploy, and scale on Volcano's global platform — free to start, with no infrastructure to manage.

Checkout more features