How pacing works
Every outbound call to a Domain draws from that Domain’s rate buckets. Each bucket carries the provider’s raw caps for three windows — 60 seconds, one hour, and one day — reduced by your configured safety margin. All three windows are enforced simultaneously: a call is sent only when it fits inside the minute, the hour, and the day budget. The budget is debited before a call is sent, never reconciled after the fact. That ordering is the guarantee: the paced dispatcher cannot exceed the provider’s limits by design, because a call with no budget behind it is simply not sent yet. Work that does not fit the current window sits in the queue and goes out when the window rolls over. Two consequences worth internalizing:- A big upload is delivered in paced slices. One million records is around twenty upload calls, and with the default Upload bucket that is roughly a minute of budget — but a busy Domain, a tightened margin, or competing work can stretch it. The batch reports honest progress the whole way.
- Synchronous calls fail fast instead of queueing. When you are waiting on an HTTP response — testing a credential, refreshing a Domain’s limits — an exhausted budget returns
429with aRetry-Afterheader rather than silently holding your request. Nothing is sent, nothing is queued; retry after the advertised wait.
The minimum interval
Window caps answer “how many calls per minute, hour and day”. Some providers also constrain how close together two calls may be — a per-second ceiling, or a concurrency limit — and that is not something a minute cap can express: twenty calls per minute is satisfied just as well by twenty calls in the first second as by one every three seconds. So a bucket may also carry a minimum interval: a floor on the gap between two consecutive calls, in milliseconds. It complements the window caps rather than replacing them. The effective spacing is the larger of the two. The caps already imply a spacing — a 20-per-minute cap is one call every three seconds — and the configured floor is folded in as whichever is bigger:SMS bucket on your Twilio Account record and spends it in the interactive lane, and what a long-code sender is actually constrained by is the gap between two consecutive messages rather than a per-hour count — so on that bucket the floor, not the window cap, is usually what sets the pace.
The value lives on the tenant record alongside every other provider cap — the same home, because it is the same kind of fact — and is seeded there from the platform’s published default in the catalog when the tenant is bound. Your safety margin is not applied to it: a margin is a multiplier on call counts, and applying it to a duration would shorten the gap — loosening the budget, again in the wrong direction.
The floor is returned on the tenant record, and it is not editable there.
min_interval_ms appears on every rate bucket in the tenancy read surface, so the value actually being enforced is readable rather than something to infer from the catalog — null means that bucket has no floor and the caps alone set the spacing. What has not changed is that it is not a PATCH field: margins, lane_overrides and lane_order remain the editable ones, and the floor is seeded from the platform’s catalog default and corrected from observed behaviour.No new rate window was introduced. The minute / hour / day ledger is exactly as it was; there is no fourth window and no per-second counter. The minimum interval is a spacing rule applied to the same ledger, which is precisely why it exists as its own field rather than as another cap.
GET /platforms/five9/domains/{tenancy_id}/headroom reports how much of each budget is left, and GET /platforms/five9/domains/{tenancy_id}/queue reports how much work is waiting and when it should drain — naming which window binds, whether a rate window, a pacing policy or a hold is the constraint, and saying unknown with a machine-readable reason rather than fabricating a time it cannot compute. Quota and progress is the full model for both, plus the pre-flight cost estimate that tells you what an upload will consume before you commit to it.
When the provider says slow down
If Five9 returns a rate-limit response anyway (another system may be spending the same Domain’s budget), GetDialed pauses only the affected operation class — the one rate bucket that was throttled — for the remainder of that window, then resumes on its own. Uploads backing off never block report reads, and vice versa. These automatic backoffs appear in the pause listing like every other hold, with a machine-readablecause naming the provider fault that opened them. They release themselves at the window boundary and cannot be lifted early — resuming into a limit the provider just enforced would only trip it again.
A text message can also wait for the clock
Budget is not the only thing that can hold outbound work. A text message is additionally checked against the recipient’s own legal sending window at the moment it reaches the front of the queue — not when its task started, because the recipient’s clock moves while the message waits here. A message outside that window is held rather than sent, with a computed release time, and the queue immediately claims the next item instead of waiting behind it. It costs no provider budget: the hold happens before the debit, so a message that did not go out did not spend anything. Quiet hours is the full model — how the recipient’s clock is worked out, what a held run looks like, and how to declare a message the window does not apply to.How queued work is merged
Two uploads of 30,000 records each, both bound for the same list, do not have to cost two of the day’s upload calls. Before anything is sent, queued work is coalesced: items heading for the same destination are merged into a single provider call, filled toward that action’s per-call ceiling. On a budget of twenty upload calls per minute and two thousand per day, that is the difference between a backlog that clears and one that does not. Work merges only when all of the following match:
Anything that differs in any one of those is dispatched on its own. Concretely: two batches adding to the same list with the same columns do merge, even across flows; the same records aimed at two different lists do not; and a list upload never merges with a DNC write, which is both a different action and a different lane.
DNC writes are the one action with nothing to say about the last two rows: a DNC write targets no list and maps no columns, so its destination is the action itself. Two DNC writes on the same Domain therefore share a destination by definition, and a bulk record set of numbers to suppress goes out as a single multi-number call.
Two ceilings bound a merged call — a record count and a payload size. The record count is published per action on the catalog as
max_records_per_call (50,000 for a Five9 list upload); the size ceiling exists because a merged call must still fit inside a request the provider will accept. Whichever binds first ends that call, and the remaining items lead the next one.
Oversized input is never truncated and never sent in violation of the limit. A request larger than the per-call ceiling is split into as many compliant calls as it takes — a guarantee, not a warning: there is no path by which an over-cap call is attempted.
One consequence to plan for: if a merged call fails outright, every item merged into it is parked together as needs_reconcile, not just the rows that caused the failure. For actions whose outcome is all-or-nothing for the whole request — DNC writes, per the supports_partial_success flag on the catalog — that is the reason to keep those groups modest rather than maximal.
Lane order and the starvation floor
Every action runs in one of four dispatch lanes. When a Domain’s budget frees up a call, the lanes are consulted in this order:interactive goes strictly first because it carries small calls a person is waiting on, where latency is the feature. compliance is strictly second: DNC obligations are measured in days of safe harbor rather than seconds, so a few minutes costs nothing — and that slack is spent accumulating. DNC writes that arrive together as part of a bulk record set are held briefly and merged into one multi-number call, since they all share the same destination. A single DNC write issued from a flow step has nothing to accumulate with, so it is not held: it dispatches at its first available call. bulk and backfill then split whatever capacity remains by weight — three bulk calls for every backfill call — so a large backfill can make steady progress without ever competing with live uploads on even terms.
Strict ordering on its own would starve the bottom of the list, so it is not the whole rule. Every lane also carries a maximum wait, and it is a hard floor: once a lane’s oldest queued item has waited longer than that lane’s bound, that lane takes the next call outright — ahead of every lane above it, including interactive. A million-record flood cannot hold a DNC write past five minutes, and sustained interactive and compliance traffic cannot strand backfill work past an hour.
Queued work does not wait for a polling tick. Enqueueing work wakes the dispatcher immediately, in every lane — no lane pays a scheduling delay before its first call. Any wait you observe is the provider budget, the merge ceiling, or a hold you opened; it is not the dispatcher sleeping on your work. The one exception is releasing a hold on an otherwise idle Domain, which can lag a few minutes — see Pause is not cancel.
lane_overrides and lane_order on the Domain record — and changing it visibly changes dispatch order. The weights and the maximum waits above are platform-wide and identical for every account.
Priority: a boost inside your lane
Sendingpriority: "high" — at trigger time, or on a batch already in flight — moves that batch’s work ahead of other work in its own lane, and nothing more. High-priority bulk is served before normal bulk; it still yields to interactive and compliance.
That boundary is deliberate. Priority is not a lane override, so no amount of it can push bulk work ahead of a DNC write or in front of a call someone is waiting on. interactive stays a protected lane, which is precisely what makes it worth having.
Two practical notes:
- Any API key can set it — there is no elevated role, because the boost only reorders how your own account’s budget is spent. It cannot take capacity from another lane, another account, or another Domain, and it never changes how fast the provider accepts calls.
- It applies to work not yet claimed. Raising priority mid-flight re-orders what is still queued; calls already sent are already gone. Boosted bulk work still merges with ordinary bulk work, since coalescing follows the lane.
Paced release
By default a batch is not paced: it is delivered as fast as your Domain’s budget allows, and that is usually what you want. Paced release is the opt-in for when the real constraint is downstream of the provider — agents available to take the calls, a warm-up ramp on a new list, a callback promise you intend to keep. A policy is a ceiling of N records every M minutes:PATCH /flows/batches/{batch_id}. records_per_period accepts 1 to 1,000,000 and period_minutes accepts 1 to 1,440 — whole minutes only, since a per-second pace cannot be honoured against limits the provider itself measures per minute.
Delivery windows
Add awindow to confine delivery to particular local hours:
timezone is a required IANA name whenever a window is used. There is no default and a fixed UTC offset is not accepted — an offset is simply wrong for half the year, whereas a named zone makes the window’s boundaries daylight-saving-correct on its own. The bounds are minutes since local midnight, start inclusive and end exclusive, so 540–1020 is 09:00 until 17:00; 1440 is valid as an end, meaning local midnight. A window that would wrap past midnight is refused rather than guessed at. days takes lowercase weekday names; omit it to deliver every day.
A closing window neither rushes nor drops. Records still queued when the window closes wait, and delivery resumes at the same local start time on the next allowed day.
The rate is a ceiling, not a quota
There is no catch-up burst, ever. After a pause, an exhausted provider budget, or a closed window, delivery resumes at N per M from that moment — the lost time is not made up. This is the point of the feature rather than a limitation of it: a policy set to protect a downstream system would be worthless if the system violated it to recover lost ground.A policy must actually be a throttle
A pacing policy has to be strictly slower than the rate already available to the batch. A policy at or above that rate is refused with422, and the message names both rates so the comparison is never a mystery. It is refused rather than quietly clamped: clamping would leave the policy you set and the behaviour you get permanently out of step with no signal that it had happened. The rate compared against is the budget after your safety margin, not the provider’s raw cap, so shaving a margin never becomes headroom a pacing policy can spend.
Where that ceiling cannot be resolved — an action that spends two budgets at once, for instance — the policy is accepted as written rather than refused on a technicality, and the budget itself remains the hard limit it always was.
Released records are ordinary queue work: they may merge with other batches’ work bound for the same destination, exactly as above. The paced batch still never delivers faster than its own policy.
Pause scopes
Sometimes you want dispatch held: a provider maintenance window, a campaign under review, a batch uploaded to the wrong list. Four admin-controlled scopes exist, from widest to narrowest:
Only the batch scope touches a batch’s status. A global, Domain, or lane hold does not rewrite the batches it affects — those holds are visible on the pause listing and the Domain surface instead, because the batches themselves are not individually paused; their Domain is.
Every pause — yours or the system’s — is an auditable record: who opened it, when, why, its scope and target, and (once resumed) who released it and when. Listing the records answers “what is holding dispatch right now?” in one call.
Pause is not cancel
Pausing deletes nothing and cancels nothing. Work already dispatched stays dispatched; work not yet dispatched stays queued, exactly where it was. Resume releases the hold and dispatch continues from precisely where it stopped — nothing is re-sent, so pausing and resuming can never contact the same person twice. A pause can carry an optionalduration_seconds (1 second up to one week), which schedules an automatic resume. That resume is applied the next time dispatch reads the hold, not by a clock ticking alongside it: on a Domain with queued work it takes effect promptly once the time passes, and on a fully idle Domain it can lag by a few minutes. This is how every scope releases, batch included — and when a batch’s scheduled resume fires, the batch’s status is restored to its pre-pause value exactly as an explicit resume would restore it. A scheduled resume can be pushed further out before it fires — forward only; shortening a hold is an explicit resume. An open-ended pause simply holds until someone resumes it.
The paused batch status is not terminal. It is a state a batch returns from: resuming restores the exact status the batch held before the pause — a batch paused while queued goes back to queued, never promoted to running.
A hold with an expiry is folded into the Domain’s estimated completion — the queue still has to drain after the hold lifts, so the wait is added to the drain rather than replacing it. An open-ended hold has no expiry to fold in, so completion reads null with the reason paused_indefinitely instead of a guessed time.
What needs_reconcile means
Uploads are sent exactly once — never blindly retried, because retrying an upload without knowing whether the first attempt landed risks contacting the same person twice. That honesty has a corner case: the call went out, and no confirmation came back.
A job in that situation is parked as needs_reconcile: we sent the upload but did not receive a confirmation, so we are not guessing. These rows are held, not retried. The status is not terminal — it is an explicit “outcome unknown” that will be resolved against the provider’s own records in a later release, and the rest of the batch keeps processing normally in the meantime. If you see needs_reconcile on a few rows of a large batch, nothing is stuck: the ambiguous slice is quarantined and everything else continues.
Batched delivery waits honestly
A flow step using batched delivery hands its records to the paced dispatcher and waits for the real outcome. On a busy or paused Domain that wait can be long — hours, if the budget demands it — and that is by design: the step reports the provider’s actual result (or an honest non-terminal status such asneeds_reconcile), never a fabricated timeout failure for work the provider may still complete.
Next steps
Pause & resume API
Every pause, resume, listing, and extension endpoint, with bounds and error cases.
Change priority or pacing
Re-order or re-pace a batch that is already queued or in flight.
Platform tenancy
The Domain record whose rate buckets and margins set the budget.
Batches, jobs, and executions
Every status in the chain, including
paused and needs_reconcile.Bulk ingestion
Stage millions of records ahead of time; dispatch is paced when you trigger.