# At-least-once delivery with deduplication guard

- **Pattern:** `ab-002458` (`sending-pipeline-infrastructure.queue-architecture.at-least-once-dedup`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002458
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002458)

## Why it matters

At-least-once delivery guarantees that a job is processed at least once — it does not guarantee it is processed exactly once. If a worker sends a password-reset email, then crashes before acknowledging the job, BullMQ re-queues it and the next worker pickup sends an identical email. For transactional messages, duplicates erode user trust immediately. CWE-362 covers race conditions on shared resources; duplicate sends are precisely that class of failure. ISO-25010:2011 reliability.fault-tolerance requires the system to behave correctly despite component failures.

## Severity rationale

High because worker crashes during network I/O are routine events, and without a dedup guard every such crash produces a duplicate transactional email delivered to real recipients.

## Remediation

Add a pre-flight dedup check at the start of the job processor in `workers/email.worker.ts`, before calling the ESP:

```ts
async function processEmailJob(job: Job<EmailJobData>) {
  const { campaignId, recipientId } = job.data

  const existing = await db.emailSendLog.findFirst({
    where: { campaignId, recipientId, status: 'sent' }
  })
  if (existing) {
    logger.info({ campaignId, recipientId }, 'Duplicate job — skipping')
    return
  }

  await db.emailSendLog.create({
    data: { campaignId, recipientId, status: 'sending', jobId: job.id }
  })

  const result = await esp.send(job.data)

  await db.emailSendLog.update({
    where: { campaignId_recipientId: { campaignId, recipientId } },
    data: { status: 'sent', espMessageId: result.messageId, sentAt: new Date() }
  })
}
```

## Detection

- **ID:** `at-least-once-dedup`
- **Severity:** `high`
- **What to look for:** Enumerate all deduplication mechanisms in the job processing logic: (1) `sent_at` timestamp check before sending, (2) idempotency key stored in a persistent database, (3) dedup ID passed to the ESP API. Count how many of these mechanisms are implemented. In an at-least-once delivery system, a job can be processed more than once if the worker crashes mid-execution before acknowledging.
- **Pass criteria:** Before sending an email, the worker checks a persistent store to verify the message has not already been sent (by campaign ID + recipient ID, or a stored idempotency key). At least 1 deduplication mechanism must be present. Report even on pass: describe which dedup mechanism is used and where it is implemented.
- **Fail criteria:** No deduplication guard exists. If a worker crashes after sending but before acknowledging the job, the next worker pickup sends the same email again. Transactional emails (password resets, order confirmations) would be sent twice.
- **Skip (N/A) when:** The queue explicitly provides exactly-once delivery guarantees AND that guarantee is verified in the configuration — note the mechanism in the detail field.
- **Cross-reference:** The retry-error-handling category's idempotent-retries check verifies idempotency at the retry level — this check verifies the initial dedup guard at the queue level.
- **Detail on fail:** `"Worker marks job complete only after ESP responds, but no dedup check before sending — worker crash causes duplicate send"` or `"No idempotency key passed to SendGrid — same job retried after network timeout sends email twice"`
- **Remediation:** Add a deduplication check using a database record before each send:

  ```ts
  // workers/email.worker.ts
  async function processEmailJob(job: Job<EmailJobData>) {
    const { campaignId, recipientId, templateId, mergeFields } = job.data

    // Dedup check — was this already sent?
    const existing = await db.emailSendLog.findFirst({
      where: { campaignId, recipientId, status: 'sent' }
    })

    if (existing) {
      logger.info({ campaignId, recipientId }, 'Duplicate job detected — skipping send')
      return // Acknowledge job without resending
    }

    // Mark as in-flight before calling ESP
    await db.emailSendLog.create({
      data: { campaignId, recipientId, templateId, status: 'sending', jobId: job.id }
    })

    const result = await esp.send({ to, templateId, mergeFields })

    await db.emailSendLog.update({
      where: { campaignId_recipientId: { campaignId, recipientId } },
      data: { status: 'sent', espMessageId: result.messageId, sentAt: new Date() }
    })
  }
  ```

## External references

- cwe CWE-362
- iso-25010 reliability.fault-tolerance

Taxons: data-integrity, error-resilience

HTML version: https://auditbuffet.com/patterns/ab-002458
