# Complaint rate alerting at threshold of 0.1% or less

- **Pattern:** `ab-000933` (`deliverability-engineering.warmup-reputation.complaint-rate-alerting`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000933
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000933)

## Why it matters

Google's 2024 bulk sender guidelines mandate that senders maintain complaint rates below 0.10% to avoid inbox filtering at Gmail — the platform receiving roughly one-third of all consumer email in the US. Complaint rates above 0.30% result in throttling or blocking. Without code that monitors this rate and fires an alert before crossing the threshold, the first indication of a problem is often a sharp drop in open rates, by which point Gmail's filtering is already active and may persist for days or weeks after the rate is corrected. The 0.08% early-warning threshold gives an action window before the hard limit is breached.

## Severity rationale

Medium because complaint rate spikes are frequently triggered by specific campaigns rather than systemic failures, and an alert at 0.08% gives time to pause the offending campaign before Gmail's 0.10% threshold triggers automatic filtering.

## Remediation

Compute complaint rate over a rolling 24-hour window and fire alerts at two thresholds — a warning at 0.08% (pre-threshold) and a critical at 0.10% (Google's hard limit). Run this check from a scheduled job or after each FBL event batch:

```ts
const COMPLAINT_RATE_WARNING  = 0.0008  // 0.08%
const COMPLAINT_RATE_CRITICAL = 0.001   // 0.10%

export async function checkComplaintRate(domain: string): Promise<void> {
  const since = new Date(Date.now() - 24 * 60 * 60 * 1000)
  const [sent, complaints] = await Promise.all([
    db.emailLog.count({ where: { fromDomain: domain, sentAt: { gte: since } } }),
    db.complaintEvent.count({ where: { domain, receivedAt: { gte: since } } })
  ])
  if (sent === 0) return
  const rate = complaints / sent
  if (rate >= COMPLAINT_RATE_CRITICAL) {
    await sendAlert({ severity: 'critical', message: `Complaint rate ${(rate * 100).toFixed(3)}% for ${domain} — above Google's 0.1% limit` })
  } else if (rate >= COMPLAINT_RATE_WARNING) {
    await sendAlert({ severity: 'warning', message: `Complaint rate ${(rate * 100).toFixed(3)}% for ${domain} — approaching threshold` })
  }
}
```

Persist the computed rate to `DomainReputation.spamRate` so the check feeds the trend graph alongside Postmaster Tools data.

## Detection

- **ID:** `complaint-rate-alerting`
- **Severity:** `medium`
- **What to look for:** Count all alert threshold definitions for complaint rate. Search for code that computes complaint rate (number of spam complaints / number of emails sent) and triggers an alert or sends a notification when the rate exceeds a threshold. Look for constants named `COMPLAINT_RATE_THRESHOLD`, `MAX_COMPLAINT_RATE`, or similar. Google's bulk sender guidelines require complaint rates below 0.10% to avoid inbox filtering. Check if alerts fire at 0.08% or earlier (before crossing the hard limit).
- **Pass criteria:** Code computes complaint rate over a rolling window. An alert or automated action fires when complaint rate exceeds 0.10% (ideally alerting at 0.08% as an early warning).
- **Fail criteria:** No complaint rate monitoring or alerting exists. Complaint rates are only observable by manually reviewing ESP dashboards.
- **Skip (N/A) when:** The project does not process FBL or complaint events from ESPs.
- **Detail on fail:** `"No complaint rate alerting configured — team would not know if complaint rate crossed Google's 0.1% threshold until inbox delivery degraded"` or `"Complaint events processed into DB but no alert threshold logic found"`
- **Remediation:** Compute and alert on complaint rate:

  ```ts
  const COMPLAINT_RATE_WARNING = 0.0008  // 0.08% — early warning
  const COMPLAINT_RATE_CRITICAL = 0.001  // 0.10% — Google's hard limit

  export async function checkComplaintRate(
    domain: string,
    windowHours: number = 24
  ): Promise<void> {
    const since = new Date(Date.now() - windowHours * 60 * 60 * 1000)

    const [sent, complaints] = await Promise.all([
      db.emailLog.count({ where: { fromDomain: domain, sentAt: { gte: since } } }),
      db.complaintEvent.count({ where: { domain, receivedAt: { gte: since } } })
    ])

    if (sent === 0) return

    const rate = complaints / sent

    if (rate >= COMPLAINT_RATE_CRITICAL) {
      await sendAlert({
        severity: 'critical',
        message: `Complaint rate for ${domain} is ${(rate * 100).toFixed(3)}% — above Google's 0.1% threshold`,
        action: 'Pause sending immediately and investigate campaign content'
      })
    } else if (rate >= COMPLAINT_RATE_WARNING) {
      await sendAlert({
        severity: 'warning',
        message: `Complaint rate for ${domain} is ${(rate * 100).toFixed(3)}% — approaching threshold`,
        action: 'Review recent campaigns for complaint patterns'
      })
    }
  }
  ```

## External references

- external google-bulk-sender-guidelines-2024 — https://support.google.com/mail/answer/81126
- iso-25010 reliability

Taxons: observability

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