Guarantees
Understanding what the engine promises (and what it does not) helps you build reliable systems without over-engineering.
Delivery Semantics at a Glance
The engine provides three tiers of guarantee, each building on the last:
| Tier | Guarantee | Mechanism |
|---|---|---|
| Enqueue | Exactly-once | Idempotency keys + unique index |
| Dispatch | Exactly-once | Lease-based claiming with heartbeat |
| Execution | At-least-once | Retry on crash/timeout; use ctx.effect() for effectively-once side effects |
In practice: an event is admitted once, dispatched to one worker at a time, but the handler may run more than once if the process crashes or times out before completion.
Exactly-Once Enqueue
When you provide an idempotencyKey on emit(), the engine guarantees that only one set of runs is created for that key, no matter how many times you emit.
// Safe to call multiple times -- only one run is created
const runs = engine.emit('payment:charge', { amount: 99 }, {
idempotencyKey: 'charge-order-42',
})
Duplicate calls return the existing runs for that key rather than creating new ones. The caller gets a deterministic response on every attempt, making retries safe and observable. With the SQLite store, this guarantee survives restarts because the key is stored durably alongside the run.
Exactly-Once Dispatch
The dispatcher uses lease-based claiming to ensure that only one worker executes a given run at any point in time.
- A run is claimed by acquiring a time-limited lease (default 30 seconds).
- A heartbeat extends the lease periodically while the handler runs.
- If the worker dies, heartbeats stop and the lease expires, allowing another worker to reclaim the run.
This prevents two workers from executing the same run concurrently. Combined with the retry mechanism, it provides exactly-once dispatch with at-least-once execution.
At-Least-Once Handler Execution
Every event that matches a registered handler will be executed at least once. If a handler crashes, the engine retries it according to the configured retry policy.
This means your handlers may run more than once after a crash or a timeout. If that is a problem, wrap side effects in ctx.effect() (see below).
engine.register('send-invoice', 'order:completed', async (ctx) => {
const payload = ctx.payload as { orderId: string }
// This handler will run at least once.
// If the process crashes after the email is sent but before
// the run is marked completed, it will run again on recovery.
await sendInvoiceEmail(payload.orderId)
return { success: true }
})
Effectively-Once Side Effects
ctx.effect(key, fn) gives you effectively-once execution for side effects. This is the tool you reach for when a side effect must not be repeated:
engine.register('charge', 'order:placed', async (ctx) => {
const payload = ctx.payload as { total: number }
// Stripe is only charged once, even if this handler runs twice
const charge = await ctx.effect('stripe-charge', () =>
stripe.charges.create({ amount: payload.total })
)
return { success: true, payload: { chargeId: charge.id } }
})
How it works:
- First call: The engine records the effect as
started, executesfn, and stores the result ascompleted. - Subsequent attempt: The engine finds the stored
completedresult and returns it immediately,fnnever re-executes. - Crash mid-effect (marked
startedbut nevercompleted): The engine re-executesfnon the next attempt, because a partial effect cannot be trusted.
Use ctx.effect() for anything that talks to the outside world: payment charges, transactional emails, webhook calls, writes to external databases.
Crash Recovery
The engine uses lease-based heartbeating to detect and recover from crashes.
- When a run is claimed for execution, the dispatcher acquires a time-limited lease (default 30 seconds).
- A heartbeat extends the lease periodically (default every 10 seconds, or
leaseTimeoutMs / 3). - If the worker process dies, heartbeats stop and the lease expires.
- On the next engine startup, expired-lease runs are reclaimed back to
idlestate. - If a retry policy is configured and the retry budget is exhausted, the run transitions to
errored, gets the deriveddead-letterstatus, and theonDeadhook fires.
This means that no run is silently lost. Every run either completes, errors out after exhausting retries, or is recovered and retried.
const engine = createEngine({
store: { type: 'sqlite', path: './app.db' },
leaseTimeoutMs: 60_000, // 60s lease window
heartbeatIntervalMs: 20_000, // heartbeat every 20s
retry: { maxRetries: 3, delay: 5000 },
onDead: (run, error) => {
alertOps(`Run ${run.id} is dead: ${error}`)
},
})
What the Engine Does NOT Guarantee
Not exactly-once handler execution
Handlers run at least once, not exactly once. After a crash, the same handler may be invoked again for the same event. Use ctx.effect() to protect side effects that must not repeat.
Not a distributed queue
The engine is designed as a single embedded runtime using one local SQLite database. It is not a replacement for distributed message brokers like RabbitMQ or Kafka. If you need horizontally scaled workers or multi-region coordination, use a different class of tool.
Not strict ordering
Runs are claimed in approximate insertion order, but under concurrency no strict FIFO ordering is guaranteed. If you need sequential processing of related events, use concurrency: 1 or chain events through triggerEvent to enforce ordering within a correlation.