When multiple workers process a drip sequence concurrently, the order emails land in a recipient's inbox is determined by worker timing and network latency, not the sequence design. Email 3 can arrive before email 2 if workers race. For onboarding sequences where each message references the previous one, out-of-order delivery breaks the narrative and confuses recipients. CWE-362 covers race conditions on shared resources — concurrent workers competing for sequence jobs are precisely that. ISO-25010:2011 reliability.fault-tolerance requires the system to behave predictably under concurrent execution.
Low because mis-ordered emails harm user experience but do not cause data loss, security exposure, or system failure.
Schedule sequence jobs with explicit millisecond-separated delays to guarantee ESP ordering, or use BullMQ FlowProducer for strict job chains:
const DAY = 24 * 60 * 60 * 1000
await emailQueue.add('seq:step-1', { campaignId, recipientId }, { delay: 0 })
await emailQueue.add('seq:step-2', { campaignId, recipientId }, { delay: 3 * DAY })
await emailQueue.add('seq:step-3', { campaignId, recipientId }, { delay: 7 * DAY })
For same-day sequences where delay alone is insufficient, use FlowProducer in lib/flows.ts to enforce strict parent-child dependency between sequence steps.
ID: sending-pipeline-infrastructure.queue-architecture.message-ordering
Severity: low
What to look for: Enumerate all sequence-based email send paths (drip campaigns, onboarding sequences) where message order matters. For each, count the number of ordering enforcement mechanisms: delayed jobs with explicit timestamps, sequential job chains (FlowProducer), or per-recipient locking. Report the ratio of ordered paths to total sequence paths.
Pass criteria: Emails within a sequence are either scheduled with explicit timestamps separated by at least 1 second (so delivery order is determined by the ESP scheduling, not worker concurrency), or processed in a guaranteed-ordered manner via job chains. Out-of-order sends are not possible for a given recipient.
Fail criteria: Multiple workers process campaign jobs concurrently with no ordering guarantee. Email 3 could be processed and sent before email 2 due to queue starvation or worker timing.
Skip (N/A) when: The project only sends one-off emails with no drip sequences or multi-step campaigns — confirmed by the absence of sequence or drip models.
Detail on fail: "Campaign sequence jobs processed by 5 concurrent workers with no ordering constraint — email 3 can arrive before email 2" or "Delayed jobs scheduled at the same timestamp — ESP delivery order not guaranteed"
Remediation: Use BullMQ job chains or delayed scheduling with per-recipient locking:
// Schedule sequence jobs with explicit delays
await emailQueue.add('sequence:step-1', { campaignId, recipientId }, { delay: 0 })
await emailQueue.add('sequence:step-2', { campaignId, recipientId }, { delay: 3 * 24 * 60 * 60 * 1000 }) // 3 days
await emailQueue.add('sequence:step-3', { campaignId, recipientId }, { delay: 7 * 24 * 60 * 60 * 1000 }) // 7 days
// Or use BullMQ flows for strict sequential processing
import { FlowProducer } from 'bullmq'
const flow = new FlowProducer({ connection })
await flow.add({
name: 'sequence:step-3',
queueName: 'email',
data: { step: 3 },
children: [{
name: 'sequence:step-2',
queueName: 'email',
data: { step: 2 },
children: [{ name: 'sequence:step-1', queueName: 'email', data: { step: 1 } }]
}]
})