Design stable operation identities and destination-side deduplication for payments, emails and writes under at-least-once execution.
How idempotency keys prevent duplicate API side effects
A worker sends a payment request. The payment provider accepts it. Before the worker reports success, its process exits. The queue sees no successful report and eventually leases the work to another worker. Without destination-side deduplication, the customer may be charged again.
This is the lost-acknowledgement problem. Durable execution is at least once: GetRatchet can schedule another execution when the result is uncertain. Queue persistence and lease fencing do not turn a remote payment or email into an exactly-once transaction.
Give the business operation a stable identity
An idempotency key identifies the intended action, not the attempt. For example, invoice:inv_2048:collect:v1 can identify collection of one invoice. Reuse it across timeouts, worker restarts and an intentional same-operation replay. A new legitimate collection or materially different action needs a different identity.
await ratchet.enqueue({ invoiceId: 'inv_2048' }, {
name: 'collect_invoice', version: '1',
idempotencyKey: 'invoice:inv_2048:collect:v1',
});ratchet.enqueue('collect_invoice', '1', {'invoiceId': 'inv_2048'},
idempotency_key='invoice:inv_2048:collect:v1')These calls use the TypeScript and Python producers from the SDK documentation. Generate the identity from a durable business record; do not generate a new random UUID inside each retrying handler. Keep keys bounded and avoid embedding email addresses, credentials or other unnecessary personal data.
Enforce uniqueness where the side effect happens
GetRatchet exposes context.idempotencyKey to TypeScript handlers and context.idempotency_key to Python handlers. Pass that identity to the destination using its documented mechanism. Some providers accept an Idempotency-Key header; others use a request field or have no idempotency support at all. Adding an arbitrary header to an API that ignores it offers no protection.
const response = await fetch(process.env.PAYMENT_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': context.idempotencyKey,
Authorization: `Bearer ${process.env.PAYMENT_TOKEN!}`,
},
body: JSON.stringify({ invoiceId: input.invoiceId }),
signal: context.signal,
});
// Classify documented provider failures locally; never log the raw response.This is a handler fragment for a provider that explicitly supports that header, not a universal payment adapter. Store provider credentials in the worker's own environment. Verify the provider's key scope, expiration period, behavior on concurrent duplicate requests and treatment of changed input.
For a database write under your control, enforce a unique operation identifier inside the same transaction as the effect. Store a result or completion record tied to the operation. A separate SELECT followed by a write is racy: two workers can both observe absence and both perform the action. The database uniqueness constraint must arbitrate concurrent attempts, and the duplicate branch must read the prior result or wait for the active operation.
If the effect is outside your database, a local “sent” flag and the remote action are not atomic. An outbox helps reliably hand off work but does not by itself deduplicate the remote destination. For email APIs without deduplication, choose an explicit tradeoff between possible duplicates and possible loss, add reconciliation where available, and avoid promising exactly-once delivery.
Understand the two retention windows
GetRatchet's retained execution results and the provider's idempotency records have independent lifetimes. Once either relevant record expires, a sufficiently late retry or replay may behave like a fresh operation. Choose a provider retention window that covers automatic retries and your manual recovery horizon. Where that is impossible, require reconciliation before replaying old work.
The generated SDK key includes run and operation context. Equivalent input within one run can reuse it, but a new run changes the generated identity. Cross-language numeric serialization can also differ. Use an explicit shared business key when multiple producers or languages submit the same logical operation. See the worker reference for the exact generated-key fields and replay behavior.
Mistakes that defeat deduplication
- Using attempt number, current time or a fresh random value in the destination key.
- Changing the payload while reusing a key whose provider contract requires identical input.
- Reusing one key for every customer, invoice or email campaign.
- Assuming a 504 or lost connection means the remote service did nothing.
- Clearing deduplication records before the maximum retry and replay horizon.
- Treating cancellation or a non-retryable result as a rollback of an already completed effect.
Before replaying uncertain work, inspect the recorded attempt and reconcile with the destination using its operation ID. Keep the original key when recovering the same effect. Bounded retries and Retry-After control timing; circuit breakers control admission. Neither replaces destination uniqueness.