# Per-campaign rate limits configurable

- **Pattern:** `ab-000938` (`deliverability-engineering.sending-throttling.campaign-rate-limits`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000938
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000938)

## Why it matters

Not all campaigns carry the same deliverability risk. A reactivation campaign to a cold list should send at a fraction of the rate appropriate for an active engaged segment. When all campaigns share a single global rate, a high-risk campaign — cold list, broad reach, lower expected engagement — runs at the same throughput as a tight re-engagement campaign, exposing the IP's reputation to the full complaint rate of the worst-performing send. Per-campaign configurable limits let operators deliberately throttle risky campaigns without touching the global settings or pausing the system.

## Severity rationale

Low because the absence of per-campaign limits does not cause immediate delivery failure, but it removes a key operational lever for managing deliverability risk on a per-send basis without global system changes.

## Remediation

Add rate configuration fields to the campaign schema so the dispatch worker can read per-campaign limits at runtime:

```ts
// In prisma/schema.prisma
model Campaign {
  id             String          @id @default(uuid())
  name           String
  subject        String
  maxPerHour     Int?            // null = use system default
  maxDailyVolume Int?            // null = no campaign-level cap
  sendWindow     Json?           // { start: "09:00", end: "17:00", tz: "America/New_York" }
  status         CampaignStatus
  scheduledAt    DateTime?
  createdAt      DateTime @default(now())
}

// In campaign dispatch worker:
export async function getCampaignRateLimit(campaignId: string): Promise<number> {
  const campaign = await db.campaign.findUnique({ where: { id: campaignId } })
  return campaign?.maxPerHour ?? DEFAULT_CAMPAIGN_RATE
}
```

Read `maxPerHour` and `maxDailyVolume` before creating the limiter for each campaign run. Expose these fields in the campaign creation UI so marketing can self-serve conservative limits without an engineering change.

## Detection

- **ID:** `campaign-rate-limits`
- **Severity:** `low`
- **What to look for:** Count all campaign rate limit fields in the schema. Check whether individual campaigns can be configured with their own sending rate or maximum daily volume, separate from the global system limits. Look for a `maxPerHour`, `sendingRate`, or `dailyCap` field on campaign or batch job schemas. Check if campaign scheduling logic respects these per-campaign limits.
- **Pass criteria:** Campaign records include at least 1 configurable rate or volume field. The dispatching system reads these fields and enforces them per campaign.
- **Fail criteria:** All campaigns share identical global rate limits with no per-campaign configuration. Individual campaigns cannot be throttled more conservatively than the system default.
- **Skip (N/A) when:** The project sends only transactional emails with no concept of campaigns.
- **Detail on fail:** `"No per-campaign rate limit configuration found — all campaigns share global system limits with no ability to throttle specific campaigns more conservatively"` or `"Campaign schema has no rate limit fields"`
- **Remediation:** Add rate configuration to campaign schema:

  ```ts
  // Campaign schema with configurable rate
  model Campaign {
    id              String   @id @default(uuid())
    name            String
    subject         String
    maxPerHour      Int?     // null = use system default
    maxDailyVolume  Int?     // null = no campaign-level cap
    sendWindow      Json?    // { start: "09:00", end: "17:00", tz: "America/New_York" }
    status          CampaignStatus
    scheduledAt     DateTime?
    createdAt       DateTime @default(now())
  }

  // In campaign dispatch worker:
  export async function getCampaignRateLimit(campaignId: string): Promise<number> {
    const campaign = await db.campaign.findUnique({ where: { id: campaignId } })
    return campaign?.maxPerHour ?? DEFAULT_CAMPAIGN_RATE
  }
  ```

## External references

- iso-25010 performance-efficiency

Taxons: cost-efficiency

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