Message ordering preserves campaign sequence
Why it matters
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.
Severity rationale
Low because mis-ordered emails harm user experience but do not cause data loss, security exposure, or system failure.
Remediation
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.
Detection
-
ID:
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 } }] }] })
External references
- cwe · CWE-362 — Concurrent Execution Using Shared Resource With Improper Synchronization
- iso-25010:2011 · reliability.fault-tolerance — Fault Tolerance
Taxons
History
- 2026-04-18·v1.0.0·Initial import from sending-pipeline-infrastructure·automated