Calls Per Second (CPS) Limit
Overview
Every Vonage account is subject to a Calls Per Second (CPS) limit. It is a cap on how many new outbound calls can be initiated within any rolling one-second window.
The default limit is 3 CPS. Exceeding it causes the platform to reject the excess calls, typically with a 429 Too Many Requests response (REST API) or a SIP 503 Service Unavailable (SIP Trunking).
This guide explains what CPS is, why it exists, how bursts cause failures even when your total call volume is low, and how to design your system to stay within the limit reliably.
Need a higher CPS limit? Limits can be raised on request. Contact Vonage to discuss your requirements. If you use subaccounts, you can distribute limits across them yourself — see Reading and Setting Subaccount CPS Limits via the API. This redistributes your existing allowance rather than increasing it: subaccount traffic remains bound by the primary API key's cap.
Why CPS Exists
CPS is a rate-control mechanism, not a capacity limit. It protects platform stability and fair resource allocation across all accounts. Even a large account with a high concurrent call limit can saturate downstream signaling infrastructure if it initiates a large number of calls in the same second.
How Bursts Cause Failures
The most common mistake is confusing average CPS with instantaneous CPS.
Consider a system that needs to place 30 calls over 10 seconds (an average of 3 CPS). If those 30 calls are all triggered simultaneously (e.g. a batch job fires at midnight), they all arrive in the first second and 27 are rejected.
Without throttling (30 calls, all fired at t=0):
t=0s ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ← 30 attempted, 27 rejected
t=1s ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
...
With throttling (30 calls, 3 per second):
t=0s ▓▓▓ ← 3 accepted
t=1s ▓▓▓ ← 3 accepted
t=2s ▓▓▓ ← 3 accepted
...
(all 30 succeed)
The limit is evaluated at the platform edge, not averaged across a window you control. Your client-side throttling must enforce the rate before calls reach Vonage.
General Best Practices
These principles apply regardless of whether you use Voice API (VAPI) or SIP Trunking.
- Enforce the limit on your side, not Vonage's. Do not rely on retrying rejected calls. A 429/503 at scale generates its own traffic and degrades your system. Gate outbound calls before they leave your infrastructure.
Note: If your infrastructure far exceeds your CPS limit, Vonage may temporarily block your traffic to protect the platform and other customers.
-
Use a token-bucket or leaky-bucket algorithm. These algorithms are purpose-built for rate limiting. A token-bucket refills at a fixed rate (e.g. 3 tokens/second), and each call consumes one token. If the bucket is empty, the call waits. Most languages have libraries that implement this in a few lines of code.
-
The CPS limit applies to outbound calls only — but across all endpoint types. Inbound calls are not subject to the CPS limit. However, the outbound limit applies uniformly regardless of which endpoint type is being dialed. All of the following count against your CPS budget:
phone— outbound call to a PSTN numbersip— call to a SIP URI or SIP trunkwebsocket— each outbound WebSocket connection established as a call leg counts as one call toward CPSapp— call to a Client SDK/WebRTC in-app endpoint
If your application mixes endpoint types in the same second, for example, placing a phone call while simultaneously opening a websocket leg, both count. Design your throttle to track the combined outbound rate across all endpoint types, not per type.
-
Add jitter when retrying. If you do retry (e.g. on transient network errors), add random jitter (50–500 ms) to the backoff. Synchronized retries from a pool of workers can recreate the original burst.
-
Monitor and alert on 429/503 responses. Track rejection rates in your logging pipeline. A sudden spike tells you that upstream traffic is bursting. Investigate the source before requesting a CPS limit increase. To monitor real-time CPS consumption at call-creation time, use the
return_cps_on_startedparameter in yourPOST /callsrequest, orreturnCpsOnStartedin an NCCOconnectaction. See the Voice API Webhook Reference for details.
SIP Trunking: PBX-Side CPS Configuration
When using Vonage SIP Trunking, your PBX is the originator of outbound SIP INVITE messages. The CPS limit must be enforced at the PBX before calls reach the Vonage SIP gateway. Most enterprise PBX platforms have a built-in outbound call throttling or trunk group pacing feature.
Important: These settings limit the rate of new outbound SIP signaling, not active call capacity. Set them to match or stay slightly below your Vonage CPS limit to leave a safety margin for transient traffic spikes.
Note: The following configurations are illustrative. Please contact your vendor for help in creating your own configuration.
Asterisk / FreePBX
In Asterisk, outbound call rate throttling is implemented at the dialplan level using the GROUP_COUNT(${EPOCH}) mechanism, which counts calls initiated within the current second and holds excess calls in a wait loop:
[globals]
calls_per_sec=3
[OUTBOUND]
exten => _X.,1,Set(GROUP()=${EPOCH})
same => n,GotoIf($[${GROUP_COUNT(${EPOCH})}>${calls_per_sec}]?DELAY,${EXTEN},1)
same => n,Dial(PJSIP/vonage-trunk/${EXTEN})
[DELAY]
exten => _X.,1,Wait(0.5)
same => n,Goto(OUTBOUND,${EXTEN},1)
The call-limit option on a PJSIP endpoint controls maximum concurrent channels, not rate. It is useful as a ceiling but not a CPS control.
For more information, see Asterisk res_pjsip Configuration.
3CX
In 3CX, CPS can be managed using the Simultaneous Calls setting under Admin > Voice & Chat > [Trunk] > Options tab, which caps the total number of concurrent calls through the trunk (inbound + outbound combined). Setting this conservatively relative to your Vonage CPS limit provides a practical guard against bursts. Note that this controls concurrent call capacity, not initiation rate.
For more information, see 3CX SIP Trunk Options.
Cisco Unified Communications Manager (CUCM)
In a Cisco environment, CPS throttling is handled by the Cisco Unified Border Element (CUBE), the session border controller that sits between CUCM and the Vonage SIP gateway. On CUBE, use the call-spike threshold and voice service voip rate-limiting directives to enforce CPS. Within CUCM, Locations provide call admission control based on bandwidth, which governs concurrent capacity.
For more information, see Cisco CUBE Configuration Guide.
Avaya Aura / Session Manager
In Avaya environments, CPS enforcement is handled by the Avaya Session Border Controller for Enterprise (SBCE), which supports per-trunk signaling rate policies. Avaya Session Manager manages call volume through Locations and SIP Entity Links, which govern concurrent capacity per link.
For more information, see Avaya SBCE Documentation.
FreeSWITCH
FreeSWITCH enforces a global new-session rate limit via the sessions-per-second parameter in autoload_configs/switch.conf.xml. This caps the rate of all new call legs system-wide:
<!-- autoload_configs/switch.conf.xml -->
<param name="sessions-per-second" value="3"/>
<param name="max-sessions" value="1000"/>
For per-gateway CPS control, use the dialplan limit application to enforce rate limiting against a specific gateway resource.
For more information, see FreeSWITCH SBC Setup/Call Admission Control.
Voice API: Throttling Outbound Calls
When placing outbound calls via the Voice API, your application directly controls the rate of POST /calls requests. The platform will return HTTP 429 if you exceed your CPS limit on the initial POST /calls request. However, subsequent calls initiated via connect actions in the NCCO are processed asynchronously — if these exceed the CPS limit, no 429 is returned to the original request. Instead, a rejected status callback is delivered to your event URL with the detail field set to throttled:
{
"from": "442079460000",
"to": "447700900000",
"detail": "throttled, see knowledge article https://api.support.vonage.com/hc/en-us/articles/207100288",
"conversation_uuid": "CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"status": "rejected",
"direction": "outbound",
"timestamp": "2020-01-01T12:00:00.000Z"
}
Note: Make sure your event webhook handler accounts for rejected callbacks with a throttled detail, not just HTTP 429 responses.
Note: A call UUID is only generated after a call resource is created. If a call is rejected because the CPS limit is exceeded, the request is rejected before resource creation and no call UUID is generated.
Implementing a Simple Token-Bucket Throttle
The example below shows how to dispatch a batch of outbound calls while respecting the CPS limit. It uses a straightforward leaky-bucket pattern with asyncio (Python), which is representative of the logic needed in any language.
import asyncio
import httpx
import random
import time
VONAGE_API_URL = "https://api.nexmo.com/v1/calls"
CPS_LIMIT = 3 # Match your account's CPS limit
JITTER_MS = 100 # Add up to 100 ms of random jitter per call
async def place_call(client: httpx.AsyncClient, call_payload: dict) -> dict:
response = await client.post(VONAGE_API_URL, json=call_payload)
response.raise_for_status()
return response.json()
async def dispatch_calls(call_payloads: list[dict]):
"""
Dispatch a batch of outbound calls at a controlled rate.
Fires CPS_LIMIT calls per second, with per-call jitter.
"""
interval = 1.0 / CPS_LIMIT # seconds between each call slot
async with httpx.AsyncClient(headers={"Authorization": "Bearer <JWT>"}) as client:
tasks = []
for i, payload in enumerate(call_payloads):
# Sleep until the next call slot, plus random jitter
jitter = random.uniform(0, JITTER_MS / 1000)
await asyncio.sleep((interval if i > 0 else 0) + jitter)
task = asyncio.create_task(place_call(client, payload))
tasks.append(task)
print(f"[{time.strftime('%H:%M:%S')}] Dispatched call {i+1}/{len(call_payloads)}")
results = await asyncio.gather(*tasks, return_exceptions=True)
failures = [r for r in results if isinstance(r, Exception)]
if failures:
print(f"{len(failures)} call(s) failed: {failures}")
return results
# Example usage
if __name__ == "__main__":
calls = [
{
"to": [{"type": "phone", "number": "14155550100"}],
"from": {"type": "phone", "number": "14155550199"},
"ncco": [{"action": "talk", "text": "Hello from Vonage."}]
}
# ...
# repeat for each call
] * 30 # 30 calls total
asyncio.run(dispatch_calls(calls))
Key Points
interval = 1.0 / CPS_LIMITensures exactly one call slot per ~333 ms at 3 CPS. Adjust when your limit is raised.- Jitter prevents all workers in a multi-process deployment from firing at the same millisecond boundary.
asyncio.gatherallows all calls to be in-flight concurrently once dispatched — the throttle only applies to the initiation rate, not the duration.- For multi-process or distributed systems, the token bucket must be shared (e.g. via Redis with a Lua script or a dedicated rate-limit sidecar). A per-process bucket will exceed your account-wide limit if you run multiple workers.
Handling 429 Responses
async def place_call_with_retry(client, payload, max_retries=3):
for attempt in range(max_retries):
try:
return await place_call(client, payload)
except httpx.HTTPStatusError as e:
if e.response.status_code == 429 and attempt < max_retries - 1:
backoff = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Retrying in {backoff:.2f}s...")
await asyncio.sleep(backoff)
else:
raise
Use exponential backoff with jitter (not a fixed sleep) to avoid synchronized retry storms across workers.
Accounts with Subaccounts: Hierarchical CPS Enforcement
This section is only relevant if you use Vonage subaccounts (sub-API keys). If your integration uses a single primary API key, you can skip it. See the Subaccounts API overview for general subaccount management.
How the Two-Layer Model Works
CPS is enforced at two levels simultaneously:
- Sub-account limit: Each individual sub-API key has its own CPS cap. Traffic originating from that key cannot exceed it.
- Primary API key cap: The aggregate CPS across all keys (primary + every subaccount) is bounded by the primary API key's CPS setting. This is a hard ceiling on total account traffic.
Both limits apply at the same time. A call is rejected if either the subaccount limit or the primary-key cap is reached.
Example
| Key | Individual CPS Limit |
|---|---|
| Primary API key | 20 CPS |
| Sub-API key 1 | 10 CPS |
| Sub-API key 2 | 10 CPS |
| Sub-API key 3 | 10 CPS |
At any given second, sub-key 1 can fire at most 10 calls, sub-key 2 at most 10 calls, and sub-key 3 at most 10 calls, but the combined total from all three (plus any calls on the primary key itself) cannot exceed 20 CPS. The sum of subaccount limits (30) is irrelevant — the primary cap always wins.
Primary API key cap: 20 CPS
┌─────────────────────────────────────────────┐
│ Sub-key 1 │ Sub-key 2 │ Sub-key 3 │
│ ≤ 10 CPS │ ≤ 10 CPS │ ≤ 10 CPS │
│ │
│ Combined total must stay ≤ 20 CPS │
└─────────────────────────────────────────────┘
What This Means in Practice
Subaccounts share the parent's budget. If you run multiple workloads or customers on separate sub-keys, they compete for the same primary-key allowance. A traffic spike on one subaccount can cause rejections on all others, even if each individual subaccount is within its own limit.
Both SIP Trunking and VAPI calls count toward the same combined cap. A mix of SIP INVITE messages and POST /calls REST requests all draw from the same primary-key ceiling. If you use both products under the same account hierarchy, model your capacity accordingly.
The primary-key limit is the number to watch. When you request a CPS increase, make sure you are increasing the primary API key's limit (raising only a subaccount's limit while leaving the primary cap unchanged will have no effect if the primary cap is already the binding constraint).
Designing for Subaccount CPS
- Set the primary-key cap to match peak aggregate demand, not the sum of subaccount limits. If subaccounts will rarely all burst at once, a primary cap lower than the total of sub-limits is fine, but model the worst-case scenario.
- Allocate subaccount limits intentionally. Keep subaccount individual limits proportional to the traffic share you expect from each workload. Oversized subaccount limits give a false sense of headroom.
- Apply per-subaccount throttling in code, not just at the primary-key level. Even if the primary cap is generous, an unthrottled subaccount can starve others by consuming a disproportionate share.
Reading and Setting Subaccount CPS Limits via the API
Subaccount CPS limits can be read and updated programmatically. The paths mirror the Subaccounts API and, like it, include no version number. Authenticate with your primary API key and secret using HTTP Basic.
| Method | Path | Purpose |
|---|---|---|
GET |
/accounts/{primary_key}/subaccounts/{sub_key}/voice |
Read the current CPS limit |
PATCH |
/accounts/{primary_key}/subaccounts/{sub_key}/voice |
Update the CPS limit |
Both endpoints take the same two path parameters:
| Parameter | Description |
|---|---|
primary_key |
The primary API key of your account hierarchy. This is the same value returned as primary_account.api_key in the response. |
sub_key |
The API key of the subaccount you want to read or update. Only subaccounts are addressable. The primary key itself can never be used as sub_key. |
Note: These endpoints are rate limited to 3 requests per second per authenticating API key, and return 429 Too Many Requests when that rate is exceeded. This is a limit on the management requests themselves, separate from the CPS limit they configure. Pace your requests accordingly if you are updating a large number of subaccounts.
Reading the Current CPS Limit
A GET request returns the subaccount's limit together with the limit of the primary account it belongs to:
{
"api_key": "a1b2c3d4",
"cps_limit": 30,
"primary_account": {
"api_key": "abcd1234",
"cps_limit": 100
}
}
The primary account's limit is included because a call is rejected when either limit is exceeded, so both values are needed to understand a subaccount's effective headroom. See How the Two-Layer Model Works.
Setting a CPS Limit
Send a PATCH request with the new value. cps_limit must be an integer of 0 or greater:
{
"cps_limit": 50
}
Important: This endpoint uses PATCH, not PUT. Send only cps_limit in the request body. All other settings on the subaccount are left unchanged.
The response has the same shape as the GET, reflecting the new value:
{
"api_key": "a1b2c3d4",
"cps_limit": 50,
"primary_account": {
"api_key": "abcd1234",
"cps_limit": 100
}
}
A subaccount's limit can be set higher than the primary account's. The higher value has no effect while the primary limit is lower, so if you have arranged a higher primary limit with Vonage, you can configure your subaccounts before it takes effect.
Important: Allow up to 60 seconds for a change to take effect. A GET may return the new limit before it is being enforced, so a successful read is not confirmation that the new rate is active.
Error Handling
Authorization failures return 404 Not Found — the same response as for an API key that does not exist. A 404 therefore does not necessarily mean the subaccount is gone. Check that the sub_key you are addressing belongs to the account hierarchy of the primary_key in the path.
For the full endpoint definitions, including request examples and all response codes, see the Voice API Reference.
Requesting a Higher Primary Account CPS Limit
The default 3 CPS limit applies to your primary account and is sufficient for most development and low-volume production use cases. For high-volume outbound campaigns, contact center deployments, or SIP trunks serving large PBX installations, a higher limit is available.
The primary account's CPS limit cannot be changed through the API. Only subaccount limits can be set programmatically, and they remain bound by the primary account's cap. To raise the primary account's limit, contact Vonage with your use case and expected call volumes. Vonage will assess the request and provision an increased limit on your account, typically reflected within one business day.