Problem
We are investigating production complaints of OTP emails arriving after the 10-minute expiry. The affected sends are from trusted branded clients, so production logs (default level info) do show the existing success line per send:
logger.info({ to, clientId: opts.clientId }, 'Sent client-branded OTP email')
(packages/auth-service/src/email/sender.ts:135)
But that line is not enough to diagnose delivery delay:
- No timing: nothing records how long the SMTP handoff to Resend took, so a slow-but-successful
sendMail() is indistinguishable from a fast one. (The log line's own timestamp is after the handoff; there is no "start" reference in the logs — the closest proxy is the OTP row's createdAt in the better-auth DB.)
- The nodemailer
sendMail() result — including messageId and the server response — is discarded, so the line cannot be correlated with Resend-side delivery events (email.delivered / email.delivery_delayed).
- No explicit timeouts are configured on the SMTP transport (
sender.ts:46-53); we silently inherit nodemailer defaults (connectionTimeout 2 min, greetingTimeout 30 s).
Proposed change
Constraints: augment the existing per-send line; do not add extra per-email log lines for the branded path — one line per successful send, as today, just richer.
- Measure elapsed time around
transporter.sendMail() and capture its result, then fold into the existing success line: { to, clientId, elapsedMs, messageId, smtpResponse? }.
- On failure, include
elapsedMs in the existing error path (better-auth.ts:229 logs the error already; measuring inside sendOtpCode lets that line carry the duration — still one line per failure).
- Default (non-branded) template paths currently log nothing on success; give them the same single enriched completion line. This adds no noise to branded sends and closes the observability gap for the fallback path.
- Optionally set explicit
connectionTimeout / greetingTimeout / socketTimeout on the transport so hangs surface as errors on a known schedule instead of nodemailer's defaults.
Why
With duration and messageId on the existing line, we can (a) rule the SMTP handoff in or out as the slow step, and (b) join app logs to Resend's per-message delivery events to measure how often delivery latency exceeds the 10-minute OTP lifetime — separating genuine provider delay from users submitting stale codes (companion issue #184).
Problem
We are investigating production complaints of OTP emails arriving after the 10-minute expiry. The affected sends are from trusted branded clients, so production logs (default level
info) do show the existing success line per send:(
packages/auth-service/src/email/sender.ts:135)But that line is not enough to diagnose delivery delay:
sendMail()is indistinguishable from a fast one. (The log line's own timestamp is after the handoff; there is no "start" reference in the logs — the closest proxy is the OTP row'screatedAtin the better-auth DB.)sendMail()result — includingmessageIdand the server response — is discarded, so the line cannot be correlated with Resend-side delivery events (email.delivered/email.delivery_delayed).sender.ts:46-53); we silently inherit nodemailer defaults (connectionTimeout 2 min, greetingTimeout 30 s).Proposed change
Constraints: augment the existing per-send line; do not add extra per-email log lines for the branded path — one line per successful send, as today, just richer.
transporter.sendMail()and capture its result, then fold into the existing success line:{ to, clientId, elapsedMs, messageId, smtpResponse? }.elapsedMsin the existing error path (better-auth.ts:229logs the error already; measuring insidesendOtpCodelets that line carry the duration — still one line per failure).connectionTimeout/greetingTimeout/socketTimeouton the transport so hangs surface as errors on a known schedule instead of nodemailer's defaults.Why
With duration and
messageIdon the existing line, we can (a) rule the SMTP handoff in or out as the slow step, and (b) join app logs to Resend's per-message delivery events to measure how often delivery latency exceeds the 10-minute OTP lifetime — separating genuine provider delay from users submitting stale codes (companion issue #184).