Durable replays memoized checkpoints through crashes, deploys, retries, and week-long waits. External effects remain at-least-once and should use idempotency keys. Background jobs, crons, human-in-the-loop approvals and event-driven pipelines, without building queues, timers or state machines.
Each step.run is checkpointed. Crash mid-run or deploy new code — persisted steps are replayed and the run resumes where it left off.
step.sleep("2d") holds no server hostage. step.waitForEvent parks a run until an approval, payment or webhook arrives — with a timeout.
Trigger functions on a schedule — 5-field cron or 6-field with seconds. No double-fires, no catch-up storms after downtime.
Concurrency limits (exact, per key), priority, throttle, rate-limit, debounce and batching — declared per function, enforced in the queue.
Every app × environment (dev/acc/prod) is a fully isolated workspace with its own signing key. Prod actions require a confirming second click.
Live trace waterfalls split durable-time vs app-time per step. Metrics, backlog history, rerun, rerun-from-step and cancel — updates pushed over SSE.
State, queue (SKIP LOCKED), timers and history in one database. Steps persist and schedule in a single transaction — no dual writes, no Redis.
The engine never runs your code — it calls your app over signed HTTP. Install @dicabrio/durable-sdk from npm; a PHP SDK ships in the repo. Register functions by push (sync) or let Durable pull them from your app URL — the wire protocol is small enough to port in an afternoon.
Graceful drain on shutdown, worker processes that scale horizontally, admin-token auth for production dashboards, HMAC on every hop.
const onboarding = createFunction({
id: "onboarding",
trigger: { event: "user.created" },
concurrency: { limit: 2, key: "tenantId" },
handler: async ({ event, step }) => {
const user = await step.run("load-user", () =>
db.users.find(event.data.id));
await step.sleep("cooldown", "2d"); // survives deploys
const ok = await step.waitForEvent("approve", {
event: "approval.received",
match: { userId: user.id },
timeout: "7d",
});
await step.run("send-email", () => mail.send(user));
return { approved: ok !== null };
},
});$onboarding = new DurableFunction(
id: 'onboarding',
trigger: ['event' => 'user.created'],
handler: function (array $event, Step $step) {
$user = $step->run('load-user', fn () =>
loadUser($event['data']['id']));
$step->sleep('cooldown', '2d'); // survives deploys
$ok = $step->waitForEvent('approve', [
'event' => 'approval.received',
'match' => ['userId' => $user['id']],
'timeout' => '7d',
]);
$step->run('send-email', fn () => sendMail($user));
return ['approved' => $ok !== null];
},
);Same replay model, same wire protocol. If the process dies between steps, the next invocation replays the memoized results in microseconds and continues exactly where it stopped.
Ships with an embedded Postgres — dev workspaces reset on startup, then Durable discovers DURABLE_DEV_APP_URL (default localhost:3000/api/durable). No Docker.