Durable Jobs
State Store
LiveUse durable state for progress, deduplication, counters, TTL-backed markers, and locks.
Goal
Store small pieces of durable job memory without adding a separate database table for every workflow concern.
Current status
Live
This area is documented as current, user-reliable behavior.
Workflow
- 1Read state when a job starts or resumes.
- 2Set state after meaningful progress.
- 3Use TTLs for temporary markers.
- 4Use counters for progress and rate-aware flows.
- 5Use locks when only one run should own a resource at a time.
Get, set, and delete
Track progress
tsconst progressKey = `imports:${payload.importId}:progress`
await state.set(progressKey, { processed: 100, total: 500 })
const progress = await state.get<{ processed: number; total: number }>(progressKey)
if (progress.processed === progress.total) {
await state.delete(progressKey)
}TTL
Use TTL-backed state for temporary dedupe markers, short-lived locks, and status values that should expire automatically.
Temporary webhook marker
tsawait state.set('webhooks:stripe:evt_123', { processed: true }, {
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
})Counters and locks
- Use counters for import progress, retry-aware limits, and lightweight metrics.
- Use locks for work that should have one owner, such as rebuilding a customer search index or provisioning a shared resource.
- Give locks a short TTL so abandoned work can recover.
Count processed rows
tsconst processed = await state.increment(`imports:${payload.importId}:processed`)
if (processed % 100 === 0) {
await publishProgress(payload.importId, processed)
}Webhook deduplication
Dedupe with state and idempotency
tsawait stackshift.queue('webhooks').enqueue(
'processStripeWebhook',
{ eventId: event.id, type: event.type },
{ idempotencyKey: `stripe:${event.id}` }
)Expected result
Workflow progress and deduplication survive retries and process restarts.