When a customer books an appointment, the confirmation email is the first trust signal they receive. A silent failure — a network hiccup that drops the send, an SMTP timeout that never retries — leaves the customer uncertain whether the booking went through. That uncertainty generates support tickets, duplicate bookings, and chargebacks. CWE-391 (failure to handle exceptional conditions) and ISO 25010 reliability.fault-tolerance both flag exactly this gap: a delivery path with no retry is not a delivery path, it's a hope. Without at least 3 retry attempts on exponential backoff, any transient email provider outage silently voids confirmation for everyone who booked during that window.
Critical because a single transient provider error silently drops the confirmation, leaving customers with no record of their booking and businesses exposed to disputes and no-shows.
Add the email queue job inside the same function that writes the confirmed status, not in a separate polling job. Set at minimum 5 retry attempts with exponential backoff in src/jobs/email-worker.ts:
// src/lib/bookings.ts
async function confirmBooking(bookingId: string) {
const booking = await db.bookings.update(bookingId, { status: 'confirmed' })
await emailQueue.add(
'send-confirmation',
{ bookingId, customerEmail: booking.customer_email },
{ attempts: 5, backoff: { type: 'exponential', delay: 2000 } }
)
}
The queue addition must be in the same function as the status write so there is no window where status is confirmed but email is not yet scheduled.
ID: booking-notifications-reminders.confirmation-email.confirmation-sent-60s
Severity: critical
What to look for: Find the booking confirmation trigger — usually a status field change to "confirmed" or a webhook from a payment processor. Trace the code path from status change to email dispatch. Check for email send logic that executes synchronously before response or asynchronously via a job queue. Verify the send uses a retry mechanism: look for a job queue with configurable retry count (e.g., attempts: N in BullMQ, max_retries in Celery) OR an explicit retry loop with backoff (exponential or fixed delay). Count the number of configured retries. Check if the email queue job is added within the same function or transaction as the booking status change.
Pass criteria: ALL of the following must be true: (1) Confirmation emails are triggered automatically on booking confirmation status — quote the function or handler name where the trigger occurs. (2) Send logic includes retry logic with at least 3 configured attempts — report the count of retry attempts and backoff type even on pass. (3) Email dispatch is queued in the same code path as the status change, not in a separate polling job. A TODO comment or empty queue setup does NOT count as pass.
Fail criteria: Email send is not automated (requires manual trigger or polling). No retry mechanism — single attempt with no fallback. Email sent via a separate cron/polling job instead of event-driven dispatch. Retry count is fewer than 3. Email queue addition is in a separate, disconnected code path from the status change.
Skip (N/A) when: No email service dependency found in package.json (searched for sendgrid, @sendgrid/mail, nodemailer, postmark, resend, mailgun, aws-sdk SES, and no SMTP configuration found in environment files).
Detail on fail: Quote the function or handler where email is sent (or state that no automated email send was found). Describe the current flow — e.g., "Email sent synchronously in confirmBooking() at src/lib/bookings.ts:42 with no retry logic; network failures result in lost email" or "Email trigger is a daily batch job in src/jobs/email-batch.ts, not real-time".
Remediation: Use a job queue to ensure reliable delivery with retries. The queue job should be added in the same function that changes booking status:
// src/lib/bookings.ts — On booking confirmed
async function confirmBooking(bookingId: string) {
const booking = await db.bookings.update(bookingId, { status: 'confirmed' })
// Queue email with automatic retries — same code path as status change
await emailQueue.add('send-confirmation', {
bookingId,
customerEmail: booking.customer_email,
}, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 }
})
}
// src/jobs/email-worker.ts — Email job with retry built-in
emailQueue.process('send-confirmation', async (job) => {
try {
await sendConfirmationEmail(job.data.customerEmail, job.data.bookingId)
} catch (error) {
if (job.attemptsMade < job.opts.attempts) throw error // Retry
// All retries exhausted — log and alert
await errorLog.create({ bookingId: job.data.bookingId, reason: error.message })
}
})
Cross-reference: Booking Flow & Lifecycle audit checks booking status transitions. Email/SMS Compliance audit checks CAN-SPAM headers. Database Audit checks transaction integrity for the status+queue atomic operation.