Campaigns that dispatch thousands of messages within minutes create volume spikes that look identical to spam runs from the perspective of receiving mail servers. ISPs score sending behavior partly on burst patterns — a sudden jump from zero to 10,000 messages per hour is a reputation signal regardless of content quality. Volume smoothing distributes the same total campaign volume over hours instead of minutes, keeping the per-minute send rate below ISP thresholds and avoiding the spike-and-throttle pattern that damages reputation cumulatively over multiple campaigns.
Low because a single burst event typically results in temporary deferral rather than permanent blocking, but repeated burst patterns train ISP filters to treat the sending IP as a high-risk sender.
Use Bottleneck (or equivalent token-bucket library) with nested global and per-ISP limiters. The outer global limiter caps total system throughput; inner per-ISP limiters enforce provider-specific limits:
import Bottleneck from 'bottleneck'
const globalLimiter = new Bottleneck({
reservoir: 100, // 100 sends per second
reservoirRefreshAmount: 100,
reservoirRefreshInterval: 1_000,
maxConcurrent: 20
})
const ispLimiters = new Map<string, Bottleneck>()
function getIspLimiter(domain: string): Bottleneck {
if (!ispLimiters.has(domain)) {
const { maxPerHour, maxConcurrent } = getIspLimit(domain)
ispLimiters.set(domain, globalLimiter.chain(new Bottleneck({
reservoir: Math.floor(maxPerHour / 60),
reservoirRefreshAmount: Math.floor(maxPerHour / 60),
reservoirRefreshInterval: 60_000,
maxConcurrent
})))
}
return ispLimiters.get(domain)!
}
Route all outgoing email through limiter.schedule(() => sendEmail(job)) — never dispatch directly to the transport.
ID: deliverability-engineering.sending-throttling.burst-detection
Severity: low
What to look for: Count all rate limiting or volume smoothing mechanisms. Look for logic that detects when a campaign or scheduled batch would send a very large volume in a short time window and smooths it over a longer period. Check for sliding window rate limiters, token bucket implementations, or queue configurations with a maximum concurrency and a delay between jobs. Also look for campaign scheduling that distributes sends across a time window (e.g., "send 100k over 4 hours" instead of "send 100k immediately").
Pass criteria: Sending volume is smoothed over time — either via a sliding window rate limiter, a scheduled queue with throttling, or campaign dispatch logic that distributes jobs over a configured time window.
Fail criteria: No burst protection exists. Campaigns can enqueue and dispatch thousands of messages per minute with no smoothing, creating sudden volume spikes that damage reputation.
Skip (N/A) when: The project only sends transactional email (one message per user action) where bursts are naturally bounded by user activity.
Detail on fail: "No burst detection or volume smoothing — a campaign to 100k subscribers would dispatch all messages within minutes, risking ISP throttling and reputation damage" or "Queue has high concurrency (>50 workers) with no per-ISP rate limiting"
Remediation: Use a token bucket or sliding window to smooth sending:
import Bottleneck from 'bottleneck'
// Limit to 100 sends per second globally, 10 per second per ISP
const globalLimiter = new Bottleneck({
reservoir: 100,
reservoirRefreshAmount: 100,
reservoirRefreshInterval: 1_000, // refill every second
maxConcurrent: 20
})
// Per-ISP child limiters
const ispLimiters = new Map<string, Bottleneck>()
function getIspLimiter(ispDomain: string): Bottleneck {
if (!ispLimiters.has(ispDomain)) {
const limit = getIspLimit(ispDomain)
ispLimiters.set(ispDomain, globalLimiter.chain(new Bottleneck({
reservoir: Math.floor(limit.maxPerHour / 60), // Per-minute equivalent
reservoirRefreshAmount: Math.floor(limit.maxPerHour / 60),
reservoirRefreshInterval: 60_000,
maxConcurrent: limit.maxConcurrent
})))
}
return ispLimiters.get(ispDomain)!
}
export async function sendWithThrottle(
email: EmailJob
): Promise<void> {
const recipientDomain = email.to.split('@')[1].toLowerCase()
const limiter = getIspLimiter(recipientDomain)
await limiter.schedule(() => sendEmail(email))
}