Skip to content
DocsStart free

Concurrency Control (CAC)

Call Admission Control (CAC) decides whether a new call is allowed to proceed. It enforces concurrency ceilings and rate limits so a single account, user, number, or runaway loop can’t exhaust capacity or rack up spend. Every call passes through admit() in the account’s PresenceDO, where the decision is made atomically (the object is single-threaded, so there are no torn counters and no races).

CAC checks several dimensions in one decision:

DimensionSourceLimits
Account concurrencyaccount.max_in / max_out / max_dialerMax concurrent calls per direction for the whole account.
Per-user concurrencysip_user.max_simultaneousMax simultaneous calls for one user (e.g. an outbound caller).
Per-DID channelsdid.max_channelsMax concurrent calls on a single inbound number.
Sliding-window ratecall_limit rows”No more than N calls per M seconds” for a scope.

The simplest gates are counters. The account has direction-specific ceilings:

{ "id": "acc_01jf18ah3jeb5w6dfp27sgjsbt", "name": "Acme Corp", "max_in": 100, "max_out": 50, "max_dialer": 20 }

…a user has a simultaneous-call ceiling…

{ "id": "us_01jatt0336xwzg2zbf3q9pyfm5", "username": "1001", "max_simultaneous": 2 }

…and a DID has a per-number channel cap (max_channels). When a call is admitted, every relevant counter is incremented; when it ends, exactly those counters are decremented.

For “rate,” not just “concurrency,” add call_limit rules. These are time-windowed: no more than max_count calls per period_sec.

{
"id": "lim_01jwt3awpk049tcdhth91evhyz", "account_id": "acc_01jf18ah3jeb5w6dfp27sgjsbt",
"scope_kind": "number", "scope_id": null,
"direction": "out",
"period_sec": 60, "max_count": 10,
"hard": 1
}
FieldPurpose
scope_kindaccount / user / did / number.
scope_idThe subject (null for account/number-pattern scopes).
directionin / out / dialer / any.
period_sec / max_countThe sliding window: max_count calls per period_sec.
hard1 = hard block; 0 = warn (advisory).

Rate windows are time-bucketed and self-healing, so they don’t require a perfectly-clean shutdown to stay accurate. You can stack several rules (e.g. [60s, 10] and [3600s, 100]) to cap both bursts and sustained volume.

new call ─▶ PresenceDO.admit(call)
│ 1. check account ceiling for the direction
│ 2. check per-user simultaneous (if a user is bound)
│ 3. check per-DID channels (inbound)
│ 4. check every applicable sliding-window rule
all pass? ── no ──▶ { admit: false, reason } ── caller rejected / overflowed
yes ──▶ increment every touched counter, bump windows,
bind callId → {counters}, return { admit: true }

If any dimension fails, the call is not admitted and admit() returns a reason:

ReasonMeaning
account_concurrencyThe account’s directional ceiling is reached.
user_simultaneousThe user’s simultaneous-call ceiling is reached.
rate:<key>A specific sliding-window rule tripped.

When a call ends, releaseCall(callId) decrements exactly the counters that call incremented (it’s idempotent, so a duplicate BYE is harmless). Because a BYE carries only the call id, the platform keeps a small cac:<callId> → account index in the key-value store so the right per-account PresenceDO can be reached to decrement. A TTL backstops any missed BYE so a leaked call can’t hold a channel forever.

CAC and the ACD reservation fence have deliberately different failure modes, because they protect different invariants:

  • CAC fails open and reconciles. If something goes wrong, it’s better to let a call through than to wrongly block a paying customer; counters are periodically reconciled against the live dialog snapshot from the SIP signaling layer.
  • Reservations fail closed. Handing the same agent to two callers is never acceptable, so a reservation only proceeds when it can prove it’s safe.

CAC is enforced per account (no cross-reseller rollup), keeping each tenant’s admission decision local to its own object.

  • GET /cac?accountId= returns the live channel-usage view: current concurrency counters and active calls for the account.
  • POST /cac/reset clears stuck/leaked counters for an account: an ops escape hatch if a counter ever drifts.

For a step-by-step, see Set concurrency limits.