Email dispatch pipelines with a queue layer (BullMQ, SQS, Celery) introduce a window between job creation and job execution. A contact who unsubscribes or receives a hard-bounce notification after a campaign job is enqueued but before the worker processes it will still receive the email if suppression is only checked at scheduling time. Under CAN-SPAM § 5, a suppression request must be honored within 10 business days — a queue delay of minutes to hours is acceptable, but a suppression check that is only performed at enqueue time can miss suppressions that arrive during that window. GDPR Art. 21 right to object has the same gap risk: an objection processed into the suppression table after the job was queued still results in a send if the worker does not re-check.
Medium because the queue-time suppression gap is a timing vulnerability rather than a permanent miss — it creates a window for non-compliant sends proportional to campaign queue depth and processing lag.
Add a suppression check inside the worker function, immediately before the ESP send call — not only at job creation:
worker.process('send-email', async (job) => {
const { to, templateId, data } = job.data
// Re-check suppression at dispatch time — catches suppressions added after job was queued:
if (await isSuppressed(to)) {
await db.sendLog.create({
data: { to, templateId, outcome: 'suppressed_at_send', job_id: job.id }
})
return // Complete the job without sending
}
await esp.send({ to, templateId, data })
await db.sendLog.create({ data: { to, templateId, outcome: 'sent', job_id: job.id } })
})
The first suppression check at enqueue time is still useful to reduce queue depth — keep both checks. The worker-level check is the safety net for the scheduling-to-dispatch window. Ensure isSuppressed queries the same global suppression table (see ab-000872), not a per-campaign exclusion list.
ID: data-quality-list-hygiene.suppression-bounce.suppression-at-queue-time
Severity: medium
What to look for: Check when suppression is checked relative to the email dispatch pipeline. Count the number of suppression check points in the send pipeline — at least 1 check must occur at the worker/dispatch level. The safest pattern checks suppression twice: once when the job is enqueued (to avoid queueing unnecessary work) and once immediately before dispatch in the worker (to catch suppressions that occurred after the job was enqueued). If suppression is only checked at campaign scheduling time, contacts who unsubscribe or bounce between scheduling and dispatch will still receive the email. Look at the worker/consumer function that actually calls the ESP send API.
Pass criteria: The worker function that calls the ESP send API checks suppression status immediately before dispatch, not only at job creation time.
Fail criteria: Suppression is checked only when the campaign is scheduled. If a contact unsubscribes after scheduling but before the worker processes the job, they still receive the email.
Skip (N/A) when: The system sends email synchronously with no queue (check happens in the same request), or batches are small enough that scheduling-to-dispatch lag is under 1 minute.
Cross-reference: Check data-quality-list-hygiene.suppression-bounce.global-suppression-list — the suppression lookup called at queue time must be the same global table, not a per-campaign exclusion list.
Detail on fail: Example: "Queue worker sends email without re-checking suppression — contacts who unsubscribe after campaign scheduling still receive sends" or "Suppression check happens at campaign build time only, not inside the worker job handler"
Remediation: Add a suppression check inside the worker, immediately before the ESP call:
// Queue worker for email dispatch:
worker.process('send-email', async (job) => {
const { to, templateId, data } = job.data
// Re-check suppression at send time — covers suppressions added after job was queued
if (await isSuppressed(to)) {
await db.sendLog.create({
data: { to, templateId, outcome: 'suppressed_at_send', job_id: job.id }
})
return // Job completes successfully but email is not sent
}
await esp.send({ to, templateId, data })
await db.sendLog.create({ data: { to, templateId, outcome: 'sent', job_id: job.id } })
})