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 bucket | Per organization (plus per-agent and platform) |
| Enforced by | Redis sliding-window counter in front of every route | The dial gate, on POST /v2/calls only |
| Configured by | Fixed constants in the orchestration service | Per-org config, editable without a deploy |
| Response when exceeded | 429, error 1300 | 429, error 1300 |
Retry-After | 1–60 seconds (time left in the window) | Always 5 |
| 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 600/min request ceiling, but if your organization runs 20 simultaneous lines, call number 21 is refused by the concurrency gate while your request quota is still 80% unused.
Rate limits
Every /v2 route is metered. The limiter is a two-bucket weighted sliding
window in Redis, keyed per organization — not per API key. Minting more keys
does not multiply your quota; all keys for an org draw on the same counter.
Buckets
Routes are split across four buckets. Each bucket has its own independent
counter, so exhausting calls.create does not stop you reading executions.
| Bucket | Limit | Routes |
|---|---|---|
calls.create | 600 requests / minute | POST /v2/callsPOST /v2/calls/{execution_id}/stopPOST /v2/batchesPOST /v2/batches/uploadPOST /v2/batches/{batch_id}/schedule |
executions.list | 600 requests / minute | GET /v2/callsGET /v2/calls/{execution_id}GET /v2/calls/{execution_id}/transcriptGET /v2/calls/{execution_id}/recordingGET /v2/agents/{agent_id}/executionsGET /v2/batches/{batch_id}/executionsGET /v2/usage |
knowledge.upload | 20 requests / minute | POST /v2/knowledge-basesPOST /v2/knowledge-bases/{collection}/documents |
default | 1000 requests / minute | Everything not listed above |
What you can rely on, and what the ceiling is
The figures above are ceilings. What you can plan to sustain on the dial and read buckets is 500 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 — 300 requests in the first ten seconds, then quiet — briefly pushes the
estimate over a ceiling set exactly at 500 and takes 429s while still inside
its budget. Setting the ceiling at 600 lets a real client keep the 500 promise
without shaping its traffic to a metronome.
Budget against 500. Treat anything between 500 and 600 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 on some buckets; 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 default, spelled out, because "everything else" is not something
you can budget against:
- all
/v2/agentsCRUD and clone, and/v2/agents/{agent_id}/versions GET /v2/batches,GET /v2/batches/{batch_id}, and batchstop/pause/resume- every
/v2/webhooksroute, includingPOST /v2/webhooks/test, the delivery ledger reads, andPATCH /v2/agents/{agent_id}/event-subscriptions - every
/v2/knowledge-basesroute except the two uploads above /v2/phone-numbers,/v2/providersand/v2/inbound-agentsGET /v2/user/me,GET /v2/scopes,/v2/audit-logs, and all/v2/api-keysroutes
The window length is 60 seconds for every bucket.
POST /v2/calls/{execution_id}/stop is metered in the calls.create
bucket, not default. A stop-storm competes with your dialling for the same
600 requests per minute.
And knowledge.upload is 20 a minute, not 1000: one request there is a
synchronous parse-and-embed that can hold a worker for minutes, so it is
deliberately the tightest bucket 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 buckets are kept, and the previous one is weighted by how much of it still overlaps the present:
Worked example, on the calls.create bucket (limit 600):
- In minute N you sent 400 requests.
- You are now 15 seconds into minute N+1, so
weight = (60 - 15) / 60 = 0.75. - You have sent 150 requests so far this minute.
estimated = (400 × 0.75) + 150 = 300 + 150 = 450.- 450 is under 600, so the request is allowed and
X-RateLimit-Remainingreports150.
Fifteen seconds later the same 400-request history is weighted at 0.5, so the
same 150 in-minute requests estimate at 350 and you have 150 headroom again. The
window slides; it does not snap.
Response headers
Every /v2 response — success and failure — carries the trio:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The bucket's ceiling for this route |
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 limiter fails open
If Redis is unreachable, the limiter allows the request. A cache outage will
never take your integration down, and you will never see a 429 caused by our
infrastructure rather than your traffic. In that degraded state the headers
report the full limit as remaining.
The consequence for you: the headers are advisory, not a ledger. 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.
Today the three bucket limits above are the same constants for every
organization — the tier is reported for your bookkeeping and does not change the
numbers. If your ceiling is ever raised, rate_limits on GET /v2/user/me is
the field that will show it. Read it at runtime rather than hard-coding 600.
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 reaches a terminal state and its webhook lands. A 3-minute call holds its slot for 3 minutes, whether you placed it in one request or one thousand.
The slabs
A dial must acquire a slot at four levels. It is all-or-nothing: if any level is full, the levels already taken are rolled back.
| Level | What it caps | Where it is configured | Default |
|---|---|---|---|
| Platform | Every live call across all tenants | platform_configs document _id: "concurrency", field global_limit (live-editable, picked up within 60s). Falls back to the BOLNA_GLOBAL_CONCURRENCY_LIMIT environment variable | 1000 |
| Organization | Your account's live calls | organization_configs.concurrency_limits.total_system for your org. Falls back to the BOLNA_ORG_CONCURRENCY_LIMIT environment variable | 20 |
| Agent | Live calls for one agent | organization_configs.concurrency_limits.per_agent | Inherits the org cap |
| Campaign | Live calls for one campaign | max_concurrent_calls on the campaign (POST/PATCH /v2/campaigns, or the campaign settings dialog) | No campaign ceiling |
Resolved limits are cached for 60 seconds, so a capacity change takes effect within a minute on every worker without a restart or redeploy.
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 — call:
Note the shape of that example: a campaign_limit of 50 on an org_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 agent level only gates when it is set below the org limit
The per-agent counter is always tracked — it drives per-agent gauges — but it
only refuses a dial when per_agent is explicitly lower than the org cap. Set
it to something below total_system and it becomes a deliberate throttle on one
agent; leave it at or above the org cap and it can never be the reason a call is
refused.
This is why your effective ceiling is:
which is exactly the concurrency.max field returned by GET /v2/user/me.
Legacy accounts. Organizations configured before August 2026 were seeded
with the literal pair per_agent = 5 / total_system = 100. Nobody chose "5
lines per agent", so that exact pair is read as not customised and the agent
inherits the org cap. Any other per_agent value is honoured as a real cap.
The LEGACY_AGENT_LIMIT_INHERITS environment variable turns this behaviour
off.
Enforcement can be off
The features.concurrency_management_enabled flag on your organization config
(default true) decides whether the caps block anything. When it is false the
counters still increment — so metrics and the current gauge stay accurate — but
the caps are raised internally and nothing is ever refused.
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 up to 5 seconds for a line to free
up. That window absorbs a micro-burst; anything longer is worse for you than an
honest error. If nothing frees, you get:
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. The dispatcher waits
BATCH_DISPATCH_SLOT_WAIT_SECONDS (default 15 seconds) for a slot, then
hands the contact back to the pending queue to be re-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.
concurrency_limit on the batch create body is accepted and stored, but
nothing reads it. The real gate is your organization's configured limits.
There is no per-batch concurrency knob.
If Redis is unavailable
Unlike the rate limiter, the concurrency gate does not fail open. It falls back to in-process counters, which still enforce the limits — per worker process rather than fleet-wide. A background reconciler corrects any counter drift against the true in-flight call count once Redis returns.
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 the batch API 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/batches | 50,000 | 400, error 1000 |
| Rows in one CSV upload | 50,000 | 400, error 1000 |
| CSV upload size | 20 MB | 400, error 1000 |
| Contacts pending across all your batches | 250,000 | 409, error 1201 |
| Furthest a batch may be scheduled ahead | 30 days | 400, error 1000 |
The 250,000 pending-contact ceiling bounds your undialled backlog across every batch, so one partner cannot queue millions of rows and starve everyone else's dispatch. Like the rate limiter, this check fails open: if the backlog count itself times out, your batch is accepted rather than rejected over a database hiccup.
Only .csv files are accepted by POST /v2/batches/upload. At most 20 offending
row numbers are named in a validation message, however many bad rows the file
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 | Your effective ceiling — min(org_limit, agent_limit) |
concurrency.current | Calls holding a slot right now. One Redis read of the exact counter the dial gate consults. If Redis is unreachable it degrades to 0 rather than failing the request |
concurrency.agent_max | The per-agent ceiling. Only gates when it is below max |
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 calls.create bucket |
rate_limits.executions_per_minute | The executions.list bucket |
rate_limits.default_per_minute | The default bucket |
| — | knowledge.upload is a fixed platform limit and is not reported here or configurable per organization |
wallet.balance | Spendable credit: allocated minus spent, floored at zero |
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 limiter fails open, so treat this as pacing, not accounting.
Designing for the limits
Reaching more people does not need more requests. One POST /v2/batches
carrying 50,000 contacts costs a single request against calls.create. Dialling
those contacts one at a time through POST /v2/calls costs 50,000 requests and
will exhaust the bucket in the first six seconds. Use batches for volume; use
POST /v2/calls for one-off, event-triggered dials.
Poll executions, do not poll harder. GET /v2/calls and the transcript and
recording routes share the 600/min executions.list bucket. 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:
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.

