CAN-SPAM §5(a)(4) requires opt-out requests be honored within 10 business days, but queuing the request for batch processing is the wrong implementation — the correct baseline is immediate synchronous suppression at the moment the user opts out. A 24-hour queue window means opted-out users keep receiving emails throughout that window, creating both a legal violation and a genuine user harm. GDPR Art. 7(3) reinforces this: the right to withdraw consent must be as easy as giving it, and any delay between withdrawal and cessation of processing is difficult to defend. CCPA §1798.120 grants a similar opt-out right with no delay tolerance.
High because delayed opt-out processing directly violates CAN-SPAM §5(a)(4) and GDPR Art. 7(3), and every email sent during the gap is an independently actionable violation.
Make suppression synchronous: write marketingOptOut: true to the database before returning a response from the unsubscribe handler. Never queue this operation.
// app/api/unsubscribe/route.ts — synchronous suppression
export async function POST(req: Request) {
const { token } = await req.json()
await db.subscriber.update({
where: { unsubToken: token },
data: { marketingOptOut: true, optOutAt: new Date(), optOutSource: 'email-link' },
})
return Response.json({ ok: true })
}
// All marketing queries must filter before sending
const eligible = await db.subscriber.findMany({
where: { marketingOptOut: false, emailVerified: true },
})
If you use a third-party list provider (Mailchimp, Klaviyo), do not maintain a parallel database that could fall out of sync — use the provider's API as the authoritative suppression source.
ID: email-sms-compliance.unsubscribe.opt-out-10-days
Severity: high
What to look for: Enumerate every relevant item. CAN-SPAM requires that opt-out requests be honored within 10 business days. In practice, the best practice is immediate suppression — the moment a user clicks unsubscribe, they should never receive another marketing email. Check the unsubscribe handler: does it immediately mark the subscriber as opted out in the database? Or does it queue the request for later processing? Check whether the suppression flag is checked before every marketing send. If unsubscribe processing is handled by a third-party email service (e.g., Mailchimp manages the list), verify that the service processes opt-outs immediately and that your application does not send duplicate campaigns that bypass the suppression.
Pass criteria: At least 1 of the following conditions is met. Unsubscribe requests are processed immediately (within seconds of the opt-out action) and suppress all future marketing sends. There is no queue, batch job, or delay between the opt-out action and the suppression taking effect. If a third-party list manager is used, its suppression is the authoritative source and is not overridden. Before evaluating, extract and quote the relevant configuration or code patterns found. Report the count of items checked even on pass.
Fail criteria: Unsubscribe request is queued for batch processing with a delay. The application re-adds opted-out users to the send list in a subsequent import or sync. No evidence that the suppression flag is checked at send time.
Do NOT pass when: The item exists only as a placeholder, stub, or TODO comment — partial implementation does not count as passing.
Skip (N/A) when: The application sends no commercial or marketing email.
Cross-reference: For deployment and infrastructure concerns, the Deployment Readiness audit covers production configuration.
Detail on fail: Example: "Unsubscribe handler queues a job in a Bull queue for processing within 24 hours. CAN-SPAM requires honoring within 10 business days but immediate suppression is the correct implementation." or "Opt-out flag set in subscriber table but marketing sends query the users table directly without joining suppression status.".
Remediation: Make opt-out immediate and structural:
// WRONG — queuing opt-out (do not do this)
// await queue.add('unsubscribe', { email })
// CORRECT — immediate suppression
export async function POST(req: Request) {
const { token } = await req.json()
// Immediately suppress — synchronous before returning response
await db.subscriber.update({
where: { unsubToken: token },
data: {
marketingOptOut: true,
optOutAt: new Date(),
optOutSource: 'email-link',
},
})
return Response.json({ ok: true })
}
// In your marketing send logic — always filter opted-out users
const eligibleSubscribers = await db.subscriber.findMany({
where: {
marketingOptOut: false, // never send to opted-out subscribers
emailVerified: true,
},
})
If you use Mailchimp or another list provider, do not maintain a parallel subscriber list in your own database that could get out of sync. Use the provider's API to check subscription status at send time, or rely entirely on the provider's list management and never send to bypassed the provider's suppression.