Users get the same email twice
After this you can answer: Why can an idempotent queue consumer still send the same email twice?
Symptom
Section titled “Symptom”Troubleshooting scenario: a customer gets two confirmations for one booking. Logs show “SES
accepted email” twice for the same booking.confirmed:<bookingId> job, possibly with different
sqsMessageId values. There need not be an exception.
Read the error first
Section titled “Read the error first”SES accepted two sends; SQS provides at-least-once delivery, meaning repeats are possible. Under Reach / Allowed / Password, successful sends prove those calls reached SES and were allowed. A later DynamoDB failure may be Reach or Allowed; read its error. Missing allow, explicit deny and wrong password do not explain two successful sends by themselves.
Decide
Section titled “Decide”flowchart TB A["Two emails, one booking"] --> B{"Completion recorded before second check?"} B -->|"no"| C{"Did sends overlap?"} C -->|"yes"| D["Concurrent check-then-send race"] C -->|"no"| E["Send succeeded, recording failed or crashed"] B -->|"yes"| F["Check job key, table and record lifetime"] D --> G["Preserve business key and document remaining gap"] E --> GLikely causes
Section titled “Likely causes”Ranked by the implementation’s known gaps; use the timeline to choose.
- Email succeeds, then the process crashes or
markDonefails; redelivery sends again. - Two workers both read “not done” before either records completion. The later conditional write cannot undo either send.
- Wrong completion table/key, an expired completion record, or overlapping processing after an incorrectly shortened visibility timeout.
How to check
Section titled “How to check”Search /minitickets/dev/worker by job key and correlation id. Compare request ids,
timestamps and sesMessageId; distinguish “email sent” from “skipped, already done”.
Inspect the matching processed-jobs item privately; do not copy customer data into reports.
Set JOBS_QUEUE_URL from SQS and compare visibility with the 10-second function timeout.
export AWS_PROFILE=ticketliteaws sqs get-queue-attributes --queue-url "$JOBS_QUEUE_URL" --attribute-names VisibilityTimeoutaws lambda list-event-source-mappings --function-name minitickets-dev-workerSetQueueAttributes can reveal a changed visibility timeout. DynamoDB item-level events
require data-event logging; Event history alone cannot reconstruct the completion writes.
The worker’s SES acceptance and completion logs are the first evidence.
Restore the business key, consistent read and completion store if broken. Keep send before record: reversing them can lose email after a crash. Current code accepts the remaining crash and concurrency windows; it does not guarantee exactly-once delivery.
Prevent
Section titled “Prevent”Test sequential duplicate suppression and send/record failures. Preserve 60-second email queue visibility. A future lease could reduce concurrent sends, but cannot atomically commit with SES.
apps/worker/src/handlers/booking-confirmed.ts → handleBookingConfirmed;
Inspect completion records →.
Say it in 30 seconds
Section titled “Say it in 30 seconds”Advanced: What the completion record guarantees
DynamoProcessedJobs.isDone uses a strongly consistent read. markDone conditionally writes
only when jobKey does not exist; it remembers completion for thirty days using DynamoDB TTL.
TTL deletion is asynchronous. A retained expired item can still suppress a duplicate; once
deleted, replaying an old booking can send again. Using SQS message id as the key would miss
two publishes of the same booking. The outbox chapter explains that producer
side of duplication.
apps/worker/src/processed-jobs.ts → DynamoProcessedJobs.
Read this book online: https://book.theversehub.in/