6 views
Brokers love the phrase “exactly-once.” Production failure modes do not. Network timeouts after a commit, replay from DLQs, and dual writes across systems recreate duplicates no matter what the marketing slide claimed. Seniors assume at-least-once everywhere and make handlers truly idempotent with durable keys and side-effect fencing.
⚡ TL;DR: Treat every delivery as a potential duplicate. Persist an idempotency record before side effects (or in the same transaction). Make effects themselves keyable (refunds, emails, ledger posts). Pair with Idempotency Keys End-to-End and Lambda Powertools Idempotency.
Exactly-Once Illusions: production guidance
text
❌ Deduping only on broker offset — producer retries mint new offsets for the same business event.
1. Handler commits DB work, crashes before ACK → broker redelivers
2. ACK lost on the wire → redelivery of already-processed message
3. DLQ replay after a bugfix → intentional duplicates
4. Two consumers in a "exclusive" group during rebalance overlap
5. Upstream producer retries after 503 with a new broker message id
Idempotency record pattern
typescript
Prefer transactional outbox: write business row + idempotency marker atomically when the store allows.
// consume/idempotent.ts
export async function handleOnce(eventId: string, fn: () => Promise<void>) {
try {
await ddb.put({
Item: { pk: `idemp#${eventId}`, sk: "v1", status: "in_progress", at: Date.now() },
ConditionExpression: "attribute_not_exists(pk)",
});
} catch (e: any) {
if (e.name === "ConditionalCheckFailedException") {
const row = await ddb.get({ Key: { pk: `idemp#${eventId}`, sk: "v1" } });
if (row.Item?.status === "done") return; // ✅ safe replay
throw new Error("in_progress_elsewhere");
}
throw e;
}
try {
await fn();
await ddb.update({
Key: { pk: `idemp#${eventId}`, sk: "v1" },
UpdateExpression: "SET #s = :d",
ExpressionAttributeNames: { "#s": "status" },
ExpressionAttributeValues: { ":d": "done" },
});
} catch (err) {
await ddb.delete({ Key: { pk: `idemp#${eventId}`, sk: "v1" } }); // allow retry
throw err;
}
}
Side effects must accept keys too
| Effect | Idempotent approach |
|---|---|
| Charge card | Processor idempotency key = eventId |
| Send email | Dedup table on (template, eventId) |
| Ledger post | Unique constraint on eventId |
| S3 write | Deterministic object key |
| Start Step Functions | name = hashed eventId |
python
# effects/email.py
def send_receipt(event_id: str, to: str, body: str):
try:
table.put_item(
Item={"pk": f"email#{event_id}", "to": to},
ConditionExpression="attribute_not_exists(pk)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return # ✅ already sent
raise
ses.send_email(to=to, body=body)
Testing duplicates on purpose
typescript
Wire this into CI alongside Hypothesis for Distributed Systems style reordering tests when you can.
test("handler survives duplicate delivery", async () => {
const evt = samplePaymentEvent();
await handleOnce(evt.id, () => applyPayment(evt));
await handleOnce(evt.id, () => applyPayment(evt)); // second time no-op
expect(await ledger.count(evt.id)).toBe(1);
});
Closing checklist
✅ Dos – ✅ Assume at-least-once for every bus and queue – ✅ Key on business event id, not broker offset alone – ✅ Persist idempotency before external side effects – ✅ Make downstream APIs accept idempotency keys – ✅ Replay duplicates in automated tests ❌ Don’ts – ❌ Don’t trust broker “EOS” as end-to-end exactly-once – ❌ Don’t ACK before durable side effects complete – ❌ Don’t use random UUIDs for effect keys on retry – ❌ Don’t treat DLQ replay as a rare manual exception – ❌ Don’t confuse batch de-dup with single-message safetyRelated reading
- Idempotency Keys End-to-End: API Gateway Through Step Functions Safely
- Lambda Powertools Idempotency: DynamoDB Keys That Survive Retries
- Saga Compensations Under Partial Failure
- DynamoDB Streams Outbox
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Postgres lock_timeout Strategies: Fail Fast Instead of Cascading Wait Queues - CheatCoders