Job queues provide at-least-once delivery guarantees — if a worker crashes after sending the reminder but before acknowledging the job, the queue will re-deliver it on restart. Without an idempotency guard that reads the reminder_sent flag before proceeding, crash recovery produces a duplicate send on every failure. CWE-362 (race condition) applies: two workers processing the same job concurrently (a known BullMQ failure mode under high load) can both pass a post-send flag check and send twice. ISO 25010 reliability.fault-tolerance requires that the system produce correct outputs even under failure and recovery. The fix — checking the flag before sending, not after — trades a low-probability missed send for a guaranteed no-duplicate guarantee.
Critical because without an idempotency guard, every worker crash during a reminder send produces a guaranteed duplicate notification on queue recovery, which erodes customer trust at scale.
In src/jobs/reminder-worker.ts, check reminder_sent before sending and set it to true before making the carrier call, not after. This is the set-before-send pattern — it accepts a low-probability missed send on crash in exchange for a guaranteed no-duplicate guarantee:
const worker = new Worker('reminders', async (job) => {
if (job.name === 'send-reminder') {
const booking = await db.bookings.findUnique({ where: { id: job.data.bookingId } })
if (booking.reminder_sent) return // Already sent — skip
// Set flag BEFORE send — prevents duplicates on crash recovery
await db.bookings.update({ where: { id: booking.id }, data: { reminder_sent: true } })
try {
await sendReminderMessage(booking)
} catch (error) {
await db.deliveryLog.create({ data: { booking_id: booking.id, type: 'reminder', error: error.message } })
throw error // Queue retries; idempotency guard prevents double-send
}
}
}, { connection: redis })
ID: booking-notifications-reminders.reminders.reminder-idempotent
Severity: critical
What to look for: Find the reminder job handler (the worker that sends individual reminders). Check for: (1) A deduplication guard — does the handler check if reminder_sent is already true before proceeding? Quote the check. (2) Timing of the flag set — is reminder_sent set to true BEFORE sending (preventing duplicates on crash, but risking missed sends) or AFTER sending (risking duplicates on crash)? Either approach is acceptable if there is a compensating mechanism. (3) Does the job queue itself provide at-least-once or exactly-once semantics? Name the queue library and its delivery guarantee.
Pass criteria: Enumerate all idempotency mechanisms found. ALL of the following: (1) Handler checks reminder_sent flag before sending — quote the check code. At least 1 idempotency guard must exist. A handler that sends unconditionally without checking does NOT count as pass. (2) The flag-set timing is intentional and documented OR the flag is set atomically with the send (e.g., database transaction that marks sent + dispatches). (3) After a simulated crash (job fails mid-execution), the system produces at most 1 duplicate send. Report the count of idempotency guards found and state which approach is used even on pass.
Fail criteria: No reminder_sent check — handler sends unconditionally. No flag update at all (reminder_sent never set to true). Flag set in a separate, non-atomic step that could be skipped on crash. Handler has no error handling — a crash leaves the reminder in an unknown state.
Skip (N/A) when: Reminders are not implemented (same criteria as reminder-scheduled-job skip).
Detail on fail: Quote the handler function and describe the idempotency gap. Example: "sendReminder worker at src/jobs/reminder-worker.ts:8 sends the reminder at line 12, then sets reminder_sent=true at line 15. If the process crashes between lines 12 and 15, the reminder will be re-sent on recovery.".
Remediation: Check the flag before sending and set it atomically:
// src/jobs/reminder-worker.ts
const worker = new Worker('reminders', async (job) => {
if (job.name === 'send-reminder') {
const booking = await db.bookings.findUnique({ where: { id: job.data.bookingId } })
// Idempotency guard
if (booking.reminder_sent) {
return // Already sent — do not duplicate
}
// Set flag BEFORE sending to prevent duplicates on crash
await db.bookings.update({
where: { id: booking.id },
data: { reminder_sent: true }
})
try {
await sendReminderMessage(booking)
} catch (error) {
// Log failure but do NOT reset flag — prevents duplicate sends
await db.deliveryLog.create({
data: { booking_id: booking.id, type: 'reminder', error: error.message }
})
throw error // Queue will retry, but idempotency guard prevents double-send
}
}
}, { connection: redis })
Cross-reference: Database Audit checks transaction atomicity. Error Handling audit checks crash recovery patterns. Booking Flow & Lifecycle audit checks idempotency of status transitions.