# IP pool management for high-volume sending

- **Pattern:** `ab-000947` (`deliverability-engineering.domain-ip-strategy.ip-pool-management`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000947
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000947)

## Why it matters

At volumes above 100,000 emails per day, a single sending IP becomes both a performance bottleneck and a single point of reputation failure. A block on one IP stops all sending until it is manually removed and replaced — a process that can take hours. IP pool management distributes sending load across multiple IPs and allows the system to automatically exclude a blocked or high-bounce IP and continue sending through healthy ones, without downtime or engineering intervention. Reputation-weighted or least-used selection also prevents one IP from absorbing a disproportionate share of sends while warm-up periods complete on others.

## Severity rationale

Low because single-IP architectures are acceptable at lower volumes, but above 50,000 emails per day a single IP block creates a total sending outage that requires manual intervention to resolve.

## Remediation

Store sending IPs in the database with per-IP health and warm-up metadata, then select from the pool based on health and current load:

```ts
export async function selectSendingIp(): Promise<string> {
  const candidates = await db.sendingIp.findMany({
    where: { active: true },
    orderBy: { sentToday: 'asc' }  // Prefer least-used
  })

  if (candidates.length === 0) {
    throw new Error('No available sending IPs')
  }

  // Exclude IPs with bounce rates above 5% — they are reputation-damaged
  const healthy = candidates.filter(ip => ip.hourlyBounceRate < 0.05)

  // Fall back to any active IP if all are bouncing above threshold
  return (healthy[0] ?? candidates[0]).ipAddress
}
```

Add an `active` boolean column to the `SendingIp` table so a damaged IP can be removed from rotation by setting `active = false` — no code deployment required. Pair with the warmup schedule check from `deliverability-engineering.warmup-reputation.automated-warmup-schedule` so IPs in early warm-up are not selected for full-volume sends.

## Detection

- **ID:** `ip-pool-management`
- **Severity:** `low`
- **What to look for:** Count all sending IPs configured and enumerate the selection logic. For high-volume senders (>100k/day), check whether the codebase manages multiple sending IPs in a pool. Look for IP rotation logic, IP health tracking (per-IP bounce rate, per-IP reputation), and the ability to remove a damaged IP from the pool without downtime. Also check whether the code balances load across IPs in the pool using round-robin, least-used, or reputation-weighted selection.
- **Pass criteria:** At least 2 sending IPs are configured and managed. The system selects IPs from the pool with awareness of each IP's health and warm-up status. An IP can be removed from rotation without code changes.
- **Fail criteria:** A single sending IP is used with no pool management, or multiple IPs are used but selected randomly without regard to health or warm-up status.
- **Skip (N/A) when:** Sending volume is below 50,000 emails/day where a single dedicated IP is sufficient, or the ESP manages IP pool selection transparently.
- **Detail on fail:** `"Single IP used for all sending — no IP pool management or rotation logic found"` or `"Multiple IPs configured but selected randomly without health checks — a damaged IP remains in rotation"`
- **Remediation:** Implement IP pool selection with health awareness:

  ```ts
  export async function selectSendingIp(): Promise<string> {
    const availableIps = await db.sendingIp.findMany({
      where: {
        active: true,
        // Exclude IPs in warm-up phase that would exceed daily limit
        // (Use the canSendMore function from warmup logic)
      },
      orderBy: { sentToday: 'asc' }  // Round-robin: prefer least-used
    })

    if (availableIps.length === 0) {
      throw new Error('No available sending IPs — all pools at capacity or in cool-down')
    }

    // Prefer IPs with lowest current bounce rate
    const ranked = availableIps
      .filter(ip => ip.hourlyBounceRate < 0.05)  // Exclude high-bounce IPs
      .sort((a, b) => a.sentToday - b.sentToday)

    return ranked[0]?.ipAddress ?? availableIps[0].ipAddress
  }
  ```

## External references

- iso-25010 reliability.availability

Taxons: operational-readiness

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