A single FIFO email queue shared by transactional and marketing sends creates a hidden contention point. A bulk marketing campaign pushing 100,000 jobs can delay a password-reset email by 30 minutes — precisely the window where a user is waiting at the browser. CWE-400 covers uncontrolled resource consumption; sharing a queue is the mechanism by which a marketing send consumes the delivery window for time-sensitive transactional messages. ISO-25010:2011 performance-efficiency.resource-utilization requires that resources are allocated in proportion to priority.
Low because the failure mode is latency degradation rather than data loss or security breach, and affects only deployments running simultaneous marketing and transactional sends.
Define separate BullMQ queues and workers for transactional and marketing sends in lib/queues.ts:
export const transactionalQueue = new Queue('email:transactional', { connection })
export const marketingQueue = new Queue('email:marketing', { connection })
const transactionalWorker = new Worker('email:transactional', processEmail, {
connection,
concurrency: 10
})
const marketingWorker = new Worker('email:marketing', processEmail, {
connection,
concurrency: 2,
limiter: { max: 100, duration: 1000 }
})
Route sends at the call site based on the email type, not a shared queue name.
ID: sending-pipeline-infrastructure.queue-architecture.priority-queues
Severity: low
What to look for: Count the number of distinct email queues in the codebase. Enumerate all email types (transactional: password resets, account verification, purchase confirmations; marketing: campaigns, newsletters, re-engagement) and classify each as transactional or marketing. Determine whether they share a single queue or are separated.
Pass criteria: Transactional and marketing sends are in at least 2 separate queues (or use a priority queue where transactional jobs have higher priority). A large marketing blast of 100,000 emails does not delay password reset emails by more than 30 seconds. Do NOT pass when a single undifferentiated queue handles both types.
Fail criteria: All email jobs share a single FIFO queue. A marketing campaign sending 100,000 emails blocks password reset emails until the batch completes.
Skip (N/A) when: The application only sends one type of email (e.g., only transactional, or only marketing — not both) — confirmed by reviewing all email send paths.
Detail on fail: "Single 'email' queue used for both transactional and marketing sends — large campaigns delay time-sensitive system emails" or "No queue priority configured — password reset emails enter behind bulk campaign jobs"
Remediation: Use separate queues for different email types:
// lib/queues.ts
export const transactionalQueue = new Queue('email:transactional', { connection })
export const marketingQueue = new Queue('email:marketing', { connection })
// Workers — more concurrency on transactional, throttled marketing
const transactionalWorker = new Worker('email:transactional', processEmail, {
connection,
concurrency: 10
})
const marketingWorker = new Worker('email:marketing', processEmail, {
connection,
concurrency: 2, // Throttle to respect ESP rate limits
limiter: { max: 100, duration: 1000 } // 100 sends/sec max
})