Rate Limits & Concurrency
Two independent ceilings govern the Graine v2 API — how many requests you may send per minute, and how many phone calls may be live at once. This page explains both, how campaigns share your account's calling capacity without starving each other, how to read your limits, and how to back off correctly.
There are two different ceilings on the v2 API, and they are enforced by two different systems. Nearly every support ticket about "limits" is really a confusion between them.
| Rate limit | Concurrency limit | |
|---|---|---|
| Counts | HTTP requests to /v2/... | Live phone calls holding a line |
| Window | Rolling 60 seconds | Instantaneous — a level, not a rate |
| Scope | Per organization, per limit | Per organization (plus per-agent and platform) |
| Enforced on | Every /v2 route | POST /v2/calls only |
| Set by | Your plan — a plan can carry higher ceilings than the defaults | Your account — Graine changes it on request, without any change on your side |
| Response when exceeded | 429, error 1300 | 202 — the call is queued and dials when a line frees. 429, error 1300, only if it cannot be queued |
Retry-After | 1–60 seconds (time left in the window) | Always 5 (on the 429) |
| Read yours from | rate_limits on GET /v2/user/me | concurrency on GET /v2/user/me |
You can hit one without being anywhere near the other. Placing 400 calls in a
minute is well inside the 1200/min request ceiling, but if your organization
runs 20 simultaneous lines, call number 21 waits for a line — queued, with a
202 — while your request quota is still 80% unused.
Rate limits
Every /v2 route is metered over a rolling 60-second window, counted per
organization — not per API key. Minting more keys does not multiply your
quota; all keys for an org draw on the same counter.
Limits by group of endpoints
Routes are split across five groups. Each group has its own independent counter, so using up the limit for placing calls does not stop you reading them.
| Group | Limit | Routes |
|---|---|---|
| Placing calls | 1200 requests / minute | POST /v2/callsPOST /v2/calls/{execution_id}/stopPOST /v2/calls/{execution_id}/transferPOST /v2/campaigns |
| Reading calls | 1200 requests / minute | GET /v2/callsGET /v2/calls/{execution_id}GET /v2/calls/{execution_id}/transcriptGET /v2/calls/{execution_id}/recordingGET /v2/calls/{execution_id}/timingGET /v2/agents/{agent_id}/executionsGET /v2/usage |
| Knowledge-base uploads | 20 requests / minute | POST /v2/knowledge-basesPOST /v2/knowledge-bases/{collection}/documents |
| Transliteration | 60 requests / minute | POST /v2/transliterate |
| Everything else | 2000 requests / minute | Every route not listed above |
What you can rely on, and what the ceiling is
The figures above are ceilings. What you can plan to sustain for placing and reading calls is 1000 a minute; the ceiling sits 20% above it.
The gap is deliberate. The limiter is a sliding window that weights the
previous minute's traffic into its estimate, so a client that front-loads its
minute — 600 requests in the first ten seconds, then quiet — briefly pushes the
estimate over a ceiling set exactly at 1000 and takes 429s while still inside
its budget. Setting the ceiling at 1200 lets a real client keep the 1000 promise
without shaping its traffic to a metronome.
Budget against 1000. Treat anything between 1000 and 1200 as slack for jitter, not capacity.
Plans
The figures above are the floor, applied to every key. A key on a higher
plan may carry a higher ceiling for some groups; the number that key is
actually held to is the one GET /v2/user/me reports in rate_limits, and
the one every response's X-RateLimit-Limit header carries. Read it there
rather than assuming the floor.
Everything in the "everything else" group, spelled out, because "everything else" is not something you can budget against:
- all
/v2/agentsCRUD and clone, and/v2/agents/{agent_id}/versions - every
/v2/campaignsroute exceptPOST /v2/campaigns - every
/v2/webhooksroute, includingPOST /v2/webhooks/test, the delivery ledger reads, andPATCH /v2/agents/{agent_id}/event-subscriptions POST /v2/calls/{execution_id}/events— sending an event into a live call is 2000 a minute, not the placing-calls 1200- every
/v2/knowledge-basesroute except the two uploads above /v2/phone-numbers,/v2/providersand/v2/inbound-agentsGET /v2/user/me,GET /v2/usage/requests,GET /v2/scopes,/v2/audit-logs, and all/v2/api-keysroutes
The window length is 60 seconds for every group.
POST /v2/calls/{execution_id}/stop counts against the placing calls
limit, not the general one. A stop-storm competes with your dialling for the
same 1200 requests per minute.
Its neighbour goes the other way: POST /v2/calls/{execution_id}/events,
which signals a live call, is on the general 2000 limit — so reacting to
things happening mid-call does not eat the budget you dial with.
And knowledge-base uploads are 20 a minute, not 2000: one request there reads and indexes whole documents, which can take minutes, so it is deliberately one of the tightest buckets on the API. Batch your documents into one upload rather than looping.
The sliding window
The counter is not a fixed per-minute reset — that would let you send a double burst across a minute boundary. Two adjacent 60-second windows are counted, and the previous one is weighted by how much of it still overlaps the present:
Worked example, on the placing-calls limit (1200):
- In minute N you sent 800 requests.
- You are now 15 seconds into minute N+1, so
weight = (60 - 15) / 60 = 0.75. - You have sent 300 requests so far this minute.
estimated = (800 × 0.75) + 300 = 600 + 300 = 900.- 900 is under 1200, so the request is allowed and
X-RateLimit-Remainingreports300.
Fifteen seconds later the same 800-request history is weighted at 0.5, so the
same 300 in-minute requests estimate at 700 and you have 500 headroom. The
window slides; it does not snap.
Response headers
Every /v2 response after your key is authenticated — success and failure
— carries the trio. A 401 is answered before your organization is known, so it
carries none:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The ceiling for this route — shared with the other routes in its group |
X-RateLimit-Remaining | limit - estimated, floored at 0 |
X-RateLimit-Reset | Seconds until the current window rolls — a duration, not a Unix timestamp |
Retry-After | Present only on a 429. Seconds to wait |
X-RateLimit-Reset is a countdown in seconds, not an epoch. A client that
treats it as a timestamp will sleep until 1970 or not at all.
Inspect them against any cheap route:
When you exceed it
Status 429, with headers:
The headers are a guide, not a ledger
You will never see a 429 caused by a problem on our side rather than your
traffic. So treat the headers as advisory: do not build a client that
assumes X-RateLimit-Remaining is an exact, transactional count. Use it to pace
yourself; use the 429 and Retry-After as the authority.
Tiers
GET /v2/user/me returns a rate_limit_tier field carried on your API key.
The group limits above are the floor every key gets; a tier can carry a higher
ceiling for any group, and never a lower one. rate_limits on
GET /v2/user/me and the X-RateLimit-Limit header report the number your key
is actually held to. Read it at runtime rather than hard-coding 1200.
Concurrency limits
Concurrency is a count of simultaneously live calls, not a rate. One slot is held from the moment a dial is accepted until the call has ended and its final status is recorded. A 3-minute call holds its slot for 3 minutes, whether you placed it in one request or one thousand.
The four levels
A call needs a free slot at all four levels. If any one of them is full, the call does not start, and the slots it would have taken at the other levels stay free.
| Level | What it caps | Who sets it, and where you read it | Default for a new organization |
|---|---|---|---|
| Platform | Every call in conversation across all of Graine | Graine, as the platform grows. concurrency.global_max on GET /v2/user/me | Shared by every account, not set per account — read global_max |
| Organization | Your account's calls in conversation | Graine, for your account; ask to have it raised. Calls at once on the dashboard's Billing page | 100 |
| Agent | Calls in conversation for each of your agents — one number that applies to every agent | Graine, for your account; ask to have it raised. concurrency.agent_max on GET /v2/user/me | 3 — how it applies is below |
| Campaign | Calls in conversation for one campaign | You: max_concurrent_calls on the campaign (POST/PATCH /v2/campaigns, or the campaign settings dialog) | No campaign ceiling |
A call takes its slot when it is answered. While it rings it holds none: Graine rings more numbers than you have free slots — the fewer of your calls are answered, the more ring for each free slot. See Calls ringing beside your lines.
A change to your limits takes effect within a minute, with nothing to change on your side.
Campaigns share the account fairly
Your organization limit is a pool, not a per-campaign allowance. Several campaigns running at once draw from the same pool, and they are kept from starving one another by two mechanisms working together.
A guaranteed floor. Every campaign that is actively asking for lines is
guaranteed at least organization limit ÷ active campaigns. That share is
reserved for it whether or not it is currently using it, so a campaign that
starts after another has already filled the pool still gets its lines as calls
end — it never has to wait for the first campaign to finish.
Work conservation. A campaign may borrow beyond its floor, but only capacity that no other campaign is short of. So a single campaign running alone uses your entire limit — fairness never leaves lines idle — and the moment a second campaign has work, the first is held to what it can keep without starving it.
Nothing is ever cut off mid-call to rebalance. A campaign that is over its share simply stops starting new calls, so the pool re-balances as its current calls end — seconds to minutes, not instantly.
The same fairness applies to which contacts are selected for dialling, not just which are admitted: each campaign gets an equal turn at the queue, and a campaign that could not use its turn carries the remainder into the next one. Without this, a campaign with a large backlog of older contacts would fill every page of the queue and a newer campaign's contacts would never be reached at all.
max_concurrent_calls is a ceiling, never a reservation
Setting it can only lower what a campaign runs. It cannot raise a campaign above your organization limit, and it cannot claim capacity from another campaign. Leave it unset for no campaign ceiling — the account limit and the fair share still apply.
To see what is actually in force for a campaign — including which limit is binding right now — open the campaign in the dashboard. Its settings show, for example:
Note what that example means: a campaign ceiling of 50 on an organization limit of 20, shared with one other campaign, is really 10. Raising the campaign number further changes nothing — the account limit is what to raise.
The per-agent limit
The per-agent limit is one number that applies to each of your agents. A new organization starts at 3. How far it holds an agent back depends on what else your organization is dialling:
- While none of your running campaigns has contacts waiting to be dialled
— calls placed one at a time with
POST /v2/calls, from a workflow, or as a test call from the dashboard — each agent runs at most its per-agent limit at once. Further calls on that agent wait for a line, exactly as they do at the organization limit. - While campaigns are dialling, your organization limit is split evenly across the agents whose campaigns have contacts waiting, and each agent may run up to that share, or its per-agent limit if that is larger. One campaign on one agent can use your whole organization limit; campaigns on two agents get up to half each. The organization limit always caps the total.
A per-agent limit at or above your organization limit never holds a call back; the per-agent count is still kept for reporting.
This is why GET /v2/user/me reports:
— the most one agent runs at once while no campaign is dialling. While campaigns are dialling, an agent can run more, up to its share of the organization limit.
Older accounts. Organizations set up before late August 2026 started with a per-agent limit of 5 and an organization limit of 100. A per-agent limit of exactly 5 that is below the organization limit is read as that old default, not a choice, so those agents use the organization limit. Organizations set up from late August to early September 2026 started with both limits at 100.
Enforcement can be off
Concurrency enforcement is on for every organization by default, and Graine can
switch it off for yours. When it is off, calls are still counted — so metrics
and the current gauge stay accurate — but nothing is ever held back.
GET /v2/user/me reports this as concurrency.enforced. Read max together
with enforced: a ceiling quoted without that flag is not the truth about what
will actually be refused.
What happens when you are at your limit
It depends on how the call was submitted.
A direct dial — POST /v2/calls waits a fraction of a second for a line to
free up, then queues the call instead of refusing it: the answer is 202
with status: "scheduled" and reason: "waiting_for_a_free_line", under the
execution_id you were given, and the call is dialled the moment a line frees —
within the agent's calling hours. Do not resend it.
Only when the call cannot be queued at all do you get the refusal:
Status 429, with Retry-After: 5. When the live count or the limit cannot be
read, the message degrades to "Account concurrency limit reached; all lines are in use."
A batch contact is never rejected this way. Graine waits up to 5 seconds for a slot, then puts the contact back in the queue to be dialled later. A saturated organization cycles its backlog instead of failing it — which is why a batch of 50,000 contacts against a 20-line account is a perfectly normal thing to submit.
There is no per-batch concurrency knob. The real gate is your organization's
configured limits, and max_concurrent_calls on a campaign, which can only
lower what those limits allow.
Telling the two 429s apart
Both carry HTTP 429 and error code 1300. Distinguish them by the message:
| Message begins | Cause | What to do |
|---|---|---|
Rate limit exceeded: ... | Too many requests in 60 seconds | Sleep for Retry-After seconds, then resume. Slow your request loop |
Account concurrency limit reached ... | Too many live calls | Sleep Retry-After (5s) and retry, or stop dialling until calls complete. Sending requests faster cannot help |
A useful secondary signal: on a rate-limit 429, X-RateLimit-Remaining is 0.
On a concurrency 429 your request quota was never the problem, so the header
usually still shows headroom. Treat the message as authoritative and the header
as a hint.
Retrying a concurrency 429 in a tight loop is the single most common
integration mistake. It converts a capacity problem into a rate-limit problem
and gets you refused twice over. Back off, or better, let a campaign queue
the work for you.
Batch and upload quotas
These are hard caps on request size, enforced before anything is written. They are not rate limits and no amount of waiting changes them.
| Quota | Value | Error when exceeded |
|---|---|---|
Contacts in one POST /v2/campaigns | 50,000 | 400, error 1000 |
| Rows in one CSV upload | 50,000 | 400, error 1000 |
| CSV upload size | 20 MB | 400, error 1000 |
| Contacts waiting to be dialled across all your campaigns | 250,000 | 409, error 1201 |
Furthest a campaign's end_time may be | 365 days ahead | 400, error 1000 |
The 250,000 pending-contact ceiling bounds your undialled backlog across every campaign, so one account cannot queue millions of rows and delay everyone else's calls. Like the rate limiter, this check errs on your side: if your backlog cannot be counted at that moment, your list is accepted rather than rejected.
A contact list uploaded as a file must be a CSV. At most 20 offending rows are
named in a validation message — CSV line numbers, or positions in a JSON
contacts array — however many bad rows the list contains.
Reading your own limits
GET /v2/user/me is the one call that reports both ceilings and your live
concurrency in a single read.
Field by field:
| Field | Meaning |
|---|---|
concurrency.max | min(org_limit, agent_limit) — the most one agent runs at once while no campaign is dialling (the per-agent limit). When your per-agent limit is the lower of the two, as it is for a new organization, your organization limit is shown as Calls at once on the dashboard's Billing page |
concurrency.current | Calls holding a slot right now, across your whole organization — the same count that decides whether a new call is placed or queued. If it cannot be read at that moment it is reported as 0 rather than failing the request |
concurrency.agent_max | Your per-agent limit. It holds calls back only when it is below your organization limit |
concurrency.global_max | The platform-wide ceiling, shared by every tenant |
concurrency.enforced | false means the caps are tracked but never block |
rate_limits.calls_per_minute | The placing-calls limit |
rate_limits.executions_per_minute | The reading-calls limit |
rate_limits.default_per_minute | The limit for everything else |
| — | Knowledge-base uploads and transliteration are not reported here; the X-RateLimit-Limit header on those routes carries their figure |
wallet.balance | Spendable credit: allocated minus spent, floored at zero |
When your per-agent limit is at or above your organization limit,
concurrency.max - concurrency.current is your dialling headroom this instant.
Poll it before a burst rather than discovering the ceiling one 429 at a time.
A 402 with error 1400 means the wallet could not fund the call. That is
not a limit you can retry past — top up the account first.
Backing off correctly
The rules, in order of importance:
- Honour
Retry-After. It is a whole number of seconds and it is authoritative. - Add jitter. Without it, every client that hit the wall together retries together.
- Cap the attempts. A
429that survives five backoffs is a capacity problem, not a timing problem. - Never retry
4xxother than429. A400,402,404or409will fail identically forever.
Python
TypeScript
Pacing without waiting for a 429
Since the trio is on every response, a client can slow itself down before it is refused:
Remember the headers are advisory, so treat this as pacing, not accounting.
Designing for the limits
Reaching more people does not need more requests. One POST /v2/campaigns
carrying 50,000 contacts costs a single request against the placing-calls
limit. Dialling those contacts one at a time through POST /v2/calls costs
50,000 requests and will exhaust that limit in the first twelve seconds. Use campaigns for volume; use
POST /v2/calls for one-off, event-triggered dials.
Poll executions, do not poll harder. GET /v2/calls and the transcript,
recording and timing routes share the 1200/min reading-calls limit. Polling every call
individually every second is what exhausts it. Prefer a webhook, or list with
filters on one request instead of fanning out.
Concurrency, not the rate limit, is what sizes your throughput. Calls per hour is roughly:
where lines is how many calls can run at once: your organization limit for
your campaigns, or concurrency.max for calls placed one at a time while no
campaign is dialling.
Twenty lines at an average of 90 seconds is about 800 calls an hour. If that is short of what you need, raising your request rate will not help — the organization concurrency limit is the number to change.

