Runtime Engine
Configure durable execution for flow waits, timers, background tasks, and runtime wake delivery.
The Runtime Engine is needed when Crux must find or resume work after the current call exits: flow.waitFor(), flow.defer(), flow.after(), flow.untilIdle(), name-bound crux.flows.*, generated runtime handlers, and runtime operator tooling.
Object-bound flows remain the baseline. flow.suspend(), reviewFlow.signal(...), and reviewFlow.run({ resume }) work without runtime config when your code already has the flow handle and resumes explicitly.
Choose a runtime shape
| Shape | Use when |
|---|---|
node() | Local development, tests, single-process demos. |
node({ store: postgres() }) | Long-lived Node processes that need durable state across restarts or multiple workers. |
serverless({ store: postgres(), wake: qstash() }) | Next/Vercel-style apps where a cold function needs an external wake. |
convex() | Convex apps that want runtime work to execute inside Convex functions and scheduler. |
genericQueue() | You own a queue and want to adapt it without a bundled Crux package. |
Deferred adapters such as SQLite, RabbitMQ, Upstash Redis store, Cloudflare, Inngest, and Temporal are not first-party runtime packages yet. They are documented as recipes or future adapter shapes only.
Runtime-bound APIs
await flow.waitFor(events.documentApproved, { match: { documentId } })
await flow.defer(embedDocument, { documentId })
await flow.after(sendReminder, '2d', { userId })
await flow.untilIdle({ scope: 'current-flow' })
await crux.flows.signal('review', flowId, 'approval', payload)Durable payloads must be JSON-compatible. Pass ids, URLs, or application storage keys for large or complex values.
When a durable event resumes a waiting flow, the Runtime Engine copies the event payload into the flow snapshot. Replay reads that snapshot payload directly instead of scanning the event log.
Retention
Maintenance prunes retained runtime records in bounded batches. Configure TTLs on the runtime composer:
runtime: node({
retention: {
events: '24h',
terminalWork: '7d',
confirmedOutbox: '24h',
idempotencyKeys: '7d',
settledTimers: '24h',
settledWaiters: '24h',
terminalSnapshots: '30d',
sweepLimit: 200,
},
})Set a class to false to keep it indefinitely. Pruning never deletes pending work, leased work, suspended work, armed waiters, scheduled timers, pending outbox rows, or running/suspended flow snapshots. Older Postgres/Convex rows that were already settled before the retention stamp columns existed are eligible once their class TTL expires.
Lease fencing
Wake execution records the worker's lease token on the work row and checks that token again inside every final commit. If maintenance reclaims an expired lease and another worker finishes the item first, the stale worker exits with LEASE_LOST instead of retrying, dead-lettering, or overwriting the current owner.
Node and HTTP handler paths heartbeat the lease while target code runs. Hosts without timer support still get fencing correctness; size leaseTtlMs so normal target execution finishes before reclaim.
Related
- Guide: Next/Vercel Runtime
- Guide: Long-lived Node Runtime
- Guide: Convex Runtime
- Guide: Testing Runtime Flows
- Reference: Runtime Engine