Sequence state held only in memory — a JavaScript Map, a module-level object, or a BullMQ job payload with no database mirror — is erased by any process restart, deployment, or crash. CWE-400 and CWE-693 both apply: the system is neither protected against state loss nor recoverable after failure (iso-25010:2011 reliability.recoverability). The concrete outcome: contacts re-enter sequences from step 0, receive duplicate emails, or disappear from sequences entirely. At scale, a single deployment during a live campaign can corrupt thousands of enrollment records with no recovery path short of manually reconstructing state from email send logs — if those logs even exist.
Critical because in-memory state is permanently lost on any restart, causing duplicate sends or silent contact abandonment with no recovery path.
Write enrollment state to the database before enqueuing the job — not after. If the enqueue fails, the persisted state serves as a recovery checkpoint; if the write fails, no job is created and nothing is lost.
// Always write state BEFORE enqueuing the job
await db.contactSequence.update({
where: { id: enrollmentId },
data: {
currentStepIndex: nextStepIndex,
nextScheduledAt: scheduledTime,
status: 'active'
}
})
// Enqueue after state is persisted — failure here is recoverable
await queue.add('send-sequence-step', { enrollmentId }, { delay: delayMs })
Verify that your schema has current_step_index, next_scheduled_at, and status columns on the enrollment table — never rely on reconstructing position from the email_sends log.
ID: campaign-orchestration-sequencing.sequence-architecture.state-persistence
Severity: critical
What to look for: Check whether contact-level sequence state is written to a durable store (database or Redis with persistence). Look for: a table or collection that records which step each contact is on, when the next step should run, and the current status. Verify that the job/queue system uses job IDs or enrollment IDs that can be recovered after a process restart. Red flags: in-memory state (JavaScript Maps, module-level objects), reliance on queue job metadata as the only state store (queues can be lost), or state stored only in process memory that is rebuilt from email send history on restart.
Pass criteria: Per-contact enrollment state is stored in a durable database or Redis with AOF/RDB persistence. Process restarts do not cause contacts to restart their sequence or skip steps. Count the number of durable storage writes in the sequence advancement path — at least 1 must occur before any email is sent.
Fail criteria: Contact sequence position is stored only in-memory or inferred from email send timestamps on each run. A process restart could cause duplicate sends or lost progress. Storing state only in a BullMQ job payload without a database record does not count as pass.
Skip (N/A) when: The project has no sequence or drip campaign functionality.
Detail on fail: "Contact sequence position stored in a JavaScript Map in-memory — process restarts reset all progress" or "No enrollment table found — step position inferred from sent_emails count each run, which breaks if emails are skipped"
Remediation: Persist enrollment state to the database immediately before scheduling a job:
// Always write state BEFORE enqueuing the job
await db.contactSequence.update({
where: { id: enrollmentId },
data: {
currentStepIndex: nextStepIndex,
nextScheduledAt: scheduledTime,
status: 'active'
}
})
// Then enqueue — if this fails, the state is still persisted and can be recovered
await queue.add('send-sequence-step', { enrollmentId }, { delay: delayMs })