Skip to main content

Adaptive send-pacing

Adaptive pacing is a closed-loop rate controller for campaign sends. While a campaign runs, it reads the campaign’s own delivery receipts (DLRs) grouped per carrier (MCCMNC), computes a pace factor between a floor and 1, and applies it to the effective send rate — the messages-per-second rate and the inter-batch delay both scale down as carrier-side failures rise. Healthy traffic keeps the factor at 1 and nothing changes. A carrier that starts failing or spam-blocking drags the pace down proportionally, so the destination network sees a lighter, recoverable load instead of a flat wall of filtered traffic. It is off by default and opt-in per campaign. Campaigns without the setting are untouched.

Why the static rate alone is not enough

A campaign’s send pace is otherwise open-loop:
  • throttle_rate is a fixed messages-per-second cap. It sets a ceiling, but it never reconsiders that ceiling mid-blast.
  • ramp_up warms a sender on a fixed linear curve. The curve climbs on schedule whether the carrier is accepting the traffic or rejecting half of it.
  • The distributed rate ceiling enforces a cross-worker cap, but nothing in the pipeline lowers it when delivery goes bad.
None of these read delivery receipts. The naive failure mode is a blast that runs at its configured rate against a carrier that has started spam-blocking it — receipts arrive showing the failures, but nothing in the pipeline acts on them, so the blast keeps hammering a blocked destination until a human notices the deliverability dip and pauses it by hand. Hours of blocked traffic burn sender reputation the whole time. Adaptive pacing removes the “until a human notices” step: the receipts feed straight back into the rate.

Opt in per campaign

Set adaptive_pacing on the campaign’s variables object — the same self-serve opt-in pattern as ramp_up, send_window, trust_tier_governor, and channel_optimization. No schema or settings change is needed; a campaign row carrying the key opts in, and a campaign created without it behaves exactly as before.
A bare enable is a complete configuration — every threshold carries a default sized for normal wholesale traffic: Values outside their valid band reset to the defaults, and a max_failure_rate at or below healthy_failure_rate collapses both back to defaults — the controller keeps running even if one override was junk. Both the snake_case names above and their camelCase equivalents (adaptivePacing, healthyFailureRate, …) are accepted on the config key. To turn it off, remove the key (or set enabled: false). Legacy campaigns have no key and parse as disabled, so there is zero behaviour change for anything you have already sent.

How the pace factor works

While the campaign runs, the executor periodically groups the campaign’s own recent send records by carrier and counts three things over the rolling window:
  • terminal — messages that reached a final state in-window (the denominator),
  • failed — failure-mode terminals (failed, undelivered, rejected),
  • blocked — the spam-block subset of failed: rejected status or a spam/block/filter/4xx-class response from the destination network.
Each carrier’s stress rate is (failed + blocked) / terminal. Spam blocks are counted double — once inside failed and once again as blocked — because a carrier actively filtering your traffic costs sender reputation far more than transient undeliverable churn. The stress rate maps linearly onto the pace factor:
  • stress at or below healthy_failure_rate → factor 1 (full rate),
  • stress at or above max_failure_rate → factor min_pace_factor (the floor),
  • between them → proportional interpolation between the two.
The executor then applies the blended factor two ways:
  • effective messages per second multiply by the factor — factor 0.6 on a 50 MPS campaign sends 30 MPS,
  • inter-batch delay divides by the factor — factor 0.6 widens a 1 s gap between batches to ~1.7 s.
Each new weighted average converges via exponential smoothing (smoothing), so a single noisy window doesn’t slam the rate — the factor walks toward the reading over a couple of refreshes instead of oscillating. Three properties keep it safe to leave on:
  • Degrade-open. If the receipt read returns nothing (no rows yet, a transient error), the factor is 1 — the controller is a reputation optimiser, not a hard stop. Your credit caps and circuit-breaks still own actual halts.
  • Small samples don’t throttle. A carrier under min_sample terminals contributes factor 1 rather than a scary-looking 50% off two messages.
  • The floor is non-zero. Even the worst carrier drains at min_pace_factor of the configured rate instead of stalling the campaign.

Carrier-aware, not one global knob

Every carrier (MCCMNC) is scored independently on its own receipts, then blended into one campaign factor weighted by recent terminal traffic on that carrier:
  • A destination network carrying most of your volume dominates the blend — when it starts blocking, the whole campaign slows hard.
  • A tiny stressed carrier barely moves the blend — one bad two-percent slice doesn’t tank an otherwise healthy blast.
That is the difference from a single “campaign failure rate” gauge: a global gauge either ignores the carrier mix entirely or lets a rounding-error carrier veto the whole campaign. Traffic-weighting means the slowdown matches where your volume actually goes. Because the public what-if pricing simulator composes the same per-carrier (MCCMNC) rates, you can preview the cost of a slowed blend before launch: throttle a heavy carrier’s throughput in the simulator and compare the estimated spend trajectory with and without a pace slowdown, instead of discovering the mix mid-blast.

When not to use it

Adaptive pacing reads this campaign’s receipts. Skip it when that signal can’t exist or shouldn’t drive the rate:
  • Journey and drip campaigns. A triggered drip enrolls contacts one at a time; there is no batch volume to pace and no meaningful per-window receipt signal. Keep pacing on blast-type sends.
  • Low-volume or internal routing sends. A campaign sending a few messages per window never reaches min_sample on any carrier — the controller contributes nothing, and the per-refresh grouping is wasted work.
  • Campaigns that need to finish inside a hard window. Pacing widens the inter-batch delay when carriers stress; a blast with a fixed completion deadline should run on its static throttle_rate and accept operator monitoring instead.
  • When you want a hard stop, not a slowdown. Pace control slows a stressed carrier; it never stops the campaign by design. Use your circuit-break, credit-cap, or a manual pause for “halt this blast” semantics.

Reading the outcome

When the factor drops below 1, the campaign emits an executor log line naming the applied factor, the most-stressed carrier’s MCCMNC, its stress rate, and the number of throttled carriers — the per-carrier delivery breakdown behind those numbers is the same one the campaign dashboards surface through the analytics pipeline, so you can confirm which destination network is slowing the blast before you override anything.