When all SMS retries are exhausted and nothing happens, the customer receives no notification about their booking. A console.log on failure is not a delivery log — it evaporates on process restart and cannot be queried for support investigations. CWE-391 (unhandled failure) and CWE-770 (uncontrolled resource consumption by uncapped retry state) both apply. The compound failure is silent: the booking system shows confirmed, the customer never hears anything, and there is no record to diagnose or alert on. ISO 25010 reliability.fault-tolerance requires that delivery failures be observable and that an alternative communication path exist.
High because exhausted SMS retries with no fallback leave the customer entirely uninformed and leave operations with no structured record to investigate or alert on.
In the SMS job worker at src/jobs/sms-worker.ts, write delivery failures to a persistent log table and queue a fallback email after the final retry. The failure log must include at minimum booking_id, phone, error, attempt, and timestamp:
smsQueue.process('send-confirmation-sms', async (job) => {
try {
await twilioClient.messages.create({ body: job.data.message, from: process.env.TWILIO_PHONE!, to: job.data.phoneNumber })
} catch (error) {
await db.smsDeliveryLog.create({
data: {
booking_id: job.data.bookingId,
phone: job.data.phoneNumber,
error: error instanceof Error ? error.message : String(error),
attempt: job.attemptsMade,
timestamp: new Date(),
},
})
if (job.attemptsMade >= job.opts.attempts) {
await emailQueue.add('send-confirmation', {
bookingId: job.data.bookingId,
customerEmail: job.data.fallbackEmail,
})
} else {
throw error
}
}
})
ID: booking-notifications-reminders.sms.sms-failure-fallback
Severity: high
What to look for: Find the SMS job handler (the queue worker that processes SMS sends). Check for: (1) Failure logging — each failed attempt should create a log record with at minimum: booking_id, phone_number, error_message, attempt_number, timestamp. Search for a delivery log table or structured logging. (2) Fallback email — after the final retry fails, check whether an email is automatically queued to the customer. Trace the "all retries exhausted" code path. Verify the fallback email contains the same booking information as the SMS would have.
Pass criteria: Count all log fields present. BOTH of the following: (1) SMS failures are logged to a persistent store (database table, structured log) with at least 3 of these 5 fields: booking_id, phone_number, error_message, attempt_number, timestamp — enumerate each field found and report the count even on pass (e.g., "4 of 5 log fields present"). console.log alone does NOT count as persistent logging. (2) After all retries are exhausted, an email is automatically queued as fallback — quote the email queue addition in the exhaustion handler.
Fail criteria: Failures silently swallowed (catch block is empty or only console.log). No persistent failure log. No fallback email after final retry. Fallback email queued but missing booking details.
Skip (N/A) when: SMS is not offered (same criteria as sms-optout-persisted skip).
Detail on fail: Quote the SMS job handler and describe what happens on failure. Example: "smsWorker at src/jobs/sms-worker.ts:12 catches errors but only logs to console.log (not persistent). After final retry at line 25, no fallback email is queued — customer receives no notification if SMS fails.".
Remediation: Add persistent failure logging and automatic email fallback:
// src/jobs/sms-worker.ts
smsQueue.process('send-confirmation-sms', async (job) => {
try {
await twilioClient.messages.create({
body: job.data.message,
from: process.env.TWILIO_PHONE!,
to: job.data.phoneNumber
})
} catch (error) {
// Persistent failure log
await db.smsDeliveryLog.create({
data: {
booking_id: job.data.bookingId,
phone: job.data.phoneNumber,
error: error instanceof Error ? error.message : String(error),
attempt: job.attemptsMade,
timestamp: new Date()
}
})
if (job.attemptsMade >= job.opts.attempts) {
// All retries exhausted — queue fallback email
await emailQueue.add('send-confirmation', {
bookingId: job.data.bookingId,
customerEmail: job.data.fallbackEmail,
note: 'SMS delivery failed after all retries; sending via email'
})
} else {
throw error // Retry
}
}
})
Cross-reference: Database Audit checks log table design. Error Handling audit checks error logging patterns. Email/SMS Compliance audit checks fallback email content compliance.