> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Adaptive send-pacing: DLR-feedback rate control for campaigns

> Opt into closed-loop send pacing that reads the campaign's own delivery receipts per carrier and slows the blast when a carrier starts failing or spam-blocking, instead of hammering a blocked destination until someone notices.

# 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.

```json theme={null}
{
  "variables": {
    "adaptive_pacing": { "enabled": true }
  }
}
```

A bare enable is a complete configuration — every threshold carries a default sized for normal wholesale traffic:

| Field                  | Default | What it controls                                                                                                                                                                                                              |
| ---------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`              | —       | The only required field. Accepts `true`/`"true"`/`"1"`/`"on"`/`"yes"`.                                                                                                                                                        |
| `healthy_failure_rate` | `0.05`  | Failure-plus-block rate at or below which a carrier is healthy and gets the full send rate (factor `1`). Five percent terminal failure is ordinary churn — bad numbers, handsets switched off — and stays below this floor.   |
| `max_failure_rate`     | `0.3`   | Rate at or above which a carrier is fully throttled to the floor factor. Thirty percent sustained rejection is a carrier actively refusing the traffic.                                                                       |
| `min_pace_factor`      | `0.2`   | The floor factor. Even a fully-blocked carrier keeps draining at 20% of the configured rate so the campaign keeps making forward progress and your circuit-break and credit-cap controls remain the authority on a full halt. |
| `min_sample`           | `20`    | Terminal messages a carrier needs in-window before its reading is trusted. Below the sample floor a carrier commands factor `1` — a 1-of-1 failure on a tiny carrier is noise, not signal.                                    |
| `smoothing`            | `0.5`   | EWMA weight on the newest reading. Damps batch-to-batch oscillation from small-sample windows.                                                                                                                                |
| `window_minutes`       | `15`    | Rolling window the receipts are scored over. The controller reacts to the carrier's *current* posture, not the campaign's lifetime average.                                                                                   |

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](/guides/whatif-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](/concepts/analytics-pipeline), so you can confirm which destination network is slowing the blast before you override anything.

## Related

* [Voice & messaging pricing and throughput](/guides/voice-messaging-pricing-throughput) — the static `throttle_rate` and `ramp_up` knobs this complements.
* [Campaign limits and quiet hours](/guides/campaign-limits-quiet-hours) — admission and timing gates; pacing is the third leg, *pace*.
* [Campaign lifecycle](/concepts/campaign-lifecycle) — where campaign state transitions happen; pacing is orthogonal and survives pause/resume.
