Idempotency and dedup
Nwire handlers may run more than once — queue retries, at-least-once bus delivery, workflow replays. Idempotency means the second invocation produces the same outcome as the first, without duplicate side effects.
Where duplicates come from
| Source | Typical trigger |
|---|---|
| Queue worker | BullMQ redelivery after handler timeout |
| Event bus | NATS/JetStream or AMQP ack-after-failure |
| HTTP client | User double-clicks, proxy retries |
| Workflow timer | Saga step retried after partial failure |
Patterns
1. Natural idempotency
Some operations are idempotent by shape:
PUT /tasks/:id/complete— completing an already-complete task is a no-op.- Actor methods guarded by state —
Submission.review()rejects if notflagged.
Prefer modeling that makes retries safe without extra machinery.
2. Idempotency keys (commands)
For actions that create new resources, accept a client-supplied key:
export const createPayment = defineAction({
name: "payments.create",
description: "Miri retries checkout after a flaky Wi‑Fi drop — same key, one charge.",
input: z.object({
idempotencyKey: z.string().uuid(),
amount: z.number().positive(),
}),
handler: async (ctx) => {
const { input } = ctx;
const existing = await ctx.resolve("payments").findByKey(input.idempotencyKey);
if (existing) return existing;
// ... insert + emit
},
});Store keys with a unique index; duplicate dispatch returns the stored result.
3. Inbox dedup (events)
Cross-service consumers use defineInbox to record processed messageId values:
export const billingInbox = defineInbox({
name: "billing",
description: "Stripe webhook retries — Miri's renewal fires once per invoice id.",
});The runtime skips handler work when messageId was already processed.
4. Outbox + inbox together
Service A Service B
───────── ─────────
txn { row, outbox } ──bus──▶ inbox dedup → handler
flusher │
(at-least-once) ▼
idempotent handlerWrite path: Drizzle transactional outbox. Read path: inbox table or projection keyed by messageId.
Testing
@nwire/trust-kit scenario C09 exercises in-process dedup on retried dispatches. For integration coverage, run queue tests with RUN_INTEGRATION=1 against BullMQ or the in-memory transport with forced redelivery.