Four tiers, four different silences. And why adding a second worker made mine slower.
Building a RingCentral recording automation works in testing. A handful of calls, recordings appear, your downstream tool receives them.
Then you connect a real contact center. The first week is fine. The second week, calls start going missing, not all of them, just some. No errors. The integration shows as running. Your downstream tool is just quietly short a few recordings per shift.
There are four rate limit tiers and they fail in four different ways, which is what makes this hard to diagnose from the symptom. Exhaust one and the integration goes completely dark. Exhaust another and it keeps running happily against an empty result set. Exhaust a third and every call looks perfectly processed while no audio ever arrives. You cannot tell which one you are in by looking at whether it is working, because in three of the four cases it looks like it is.
That gap is the real cost. Not a crash, which you would fix the same day, but a slow invisible drift between what the contact center generated and what your automation processed. Every day it goes unnoticed is a day of QA sampling, transcription and analytics built on a set of calls that is quietly missing the busiest shifts, which are exactly the ones you were trying to measure.
This is orientation. RingCentral's API basics guide and authentication docs cover OAuth and the call log schema in full. What follows is not a replacement for those, it's what they don't cover.
A polling-based recording automation against RingCentral works like this:
1. Authenticate. Exchange credentials for an access token via POST /restapi/oauth/token. Tokens expire after one hour; refresh tokens are valid for a week.
2. Fetch the call log. GET /restapi/v1.0/account/{accountId}/call-log returns calls within a time window with metadata: direction, duration, agent, recording status, and per-leg detail.
3. Identify calls with recordings. Each call object includes a recording field when one is present, with an ID and a contentUri.
4. Enrich with agent and department data. GET /restapi/v1.0/account/{accountId}/extension maps extension IDs to names and departments for filtering and routing downstream.
5. Download the recording. The contentUri on the recording object points to the audio file, accessible with a valid access token.
Five steps. Four places the rate limits start to matter in ways the docs don't make obvious.
What I Assumed for a Week
The integration was falling behind, so I did the obvious thing and gave it a second worker.
It got slower.
That took me about a week to make sense of, because it is the opposite of how throughput is supposed to work. Two polling processes should move roughly twice the calls. Instead the backlog grew, and both workers started collecting 429s that neither of them was doing enough work to deserve.
The assumption underneath it: that a rate limit is a property of the process making the requests. It isn't. It's a property of the credential. Two workers against the same app credential do not get ten heavy requests a minute each. They get ten between them, and they burn a meaningful share of that budget discovering the other one exists and retrying.
Horizontal scale is the standard fix for a throughput problem, and against a per-credential limit it is actively counterproductive: the same budget divided more ways, plus retry traffic that consumes limit without moving a single call. Everything below is what I should have done instead of adding a worker.
Gap 1: There Is No Agent Filter
The call log reference documents filters for date range, direction, record type, and recording type. What it does not document is a filter by agent or extension. If your automation only cares about calls handled by a specific set of agents, the API cannot help you narrow the request. You fetch every call in the window and discard what you don't need on your side.
At low volume this is invisible. At a contact center where hundreds of thousands of calls happen per day, every polling cycle spends rate limit budget on calls that will never be processed. That budget is finite and shared across all API operations for the same credential. It gets used up before you've touched the calls you actually need.
This is the baseline pressure. Every other gap in this article adds to it.
Gap 2: Conference Calls Are Not One Recording Per Call
The call log reference describes a recording object on each call. What it doesn't explain is that for conference calls or calls with multiple transfer legs, RingCentral can attach a separate recording entry per participating agent, each with its own contentUri.
Without explicit handling, an automation will treat each entry as a distinct recording and attempt to download all of them. A call with three agents produces three download requests for audio that is, in most cases, the same conversation. The recording download endpoint sits in RingCentral's Heavy tier, the tightest rate limit in the API at 10 requests per minute. On a contact center where conference transfers are routine, the multiplier from multi-agent calls is significant.
The correct behavior is to identify the primary recording for a call and process it once. The approach that holds up across call types: prefer the recording associated with the initiator role, fall back to the first participant, then fall back to the first entry in the list.
function pickPrimaryRecording(recordings: Recording[]) {
return (
recordings.find((r) => r.agent?.role === "initiator") ??
recordings.find((r) => r.agent?.role === "participant") ??
recordings[0] ??
null
);
}
One call, one download. At high volume, this is the single most effective way to stay clear of the Heavy tier ceiling. Miss it, and the symptom is recordings that don't arrive in your downstream tool while the call log shows everything as processed.
Gap 3: RingCentral Has Four Rate Limit Tiers and Each One Fails Differently
The rate limits documentation lists RingCentral's four tiers:
| Tier | Documented Limit | Applies To |
|---|---|---|
| Auth | 5 req/min | Token refresh (/restapi/oauth/token) |
| Light | ~50 req/min | Simple reads (user info, account info) |
| Medium | ~40 req/min | Call log, extension lookups |
| Heavy | ~10 req/min | Recording downloads |
The documentation presents this as a unified system. In practice, the tiers fail independently and the symptoms look different each time.
Hit the Auth limit: token refresh fails and every subsequent API call fails with it. The integration goes fully dark. Nothing in the call log, no recordings, no enrichment, because none of those requests can authenticate.
Hit the Medium limit: call log and extension fetches stop. Calls fall out of the polling window and are missed permanently. The integration continues to look healthy because it's still running; it's just processing an empty result set.
Hit the Heavy limit: recordings don't download. Calls appear in the log, agents are identified correctly, but audio never reaches your downstream tool. No error is surfaced to the end user. This is the hardest failure mode to notice.
Three things that prevent hitting these:
1. Meter conservatively per tier. Running exactly at the documented limit leaves no margin for burst traffic. The right approach is to set internal limits at roughly 85% of what RingCentral documents, 4 instead of 5 for Auth, 35 instead of 40 for Medium, 9 instead of 10 for Heavy, and enforce each tier separately. The cost in throughput is negligible; the benefit is stability under load.
const RateLimits = {
auth: { requests: 4, windowSeconds: 60 }, // RingCentral allows 5/min
medium: { requests: 35, windowSeconds: 60 }, // RingCentral allows ~40/min
heavy: { requests: 9, windowSeconds: 60 }, // RingCentral allows 10/min
};
2. Track auth separately from data. If token refresh requests and data API calls share the same rate limit counter, a burst of call log fetches can exhaust the budget right as a token needs refreshing. Auth calls must draw from the Auth tier limit; data calls from their respective tier. Separate tracking keys enforce this:
// Data calls: tracked under the credential key
await rateLimiter.consume(credentialKey, RateLimits.medium);
// Token refresh: tracked under a separate key
await rateLimiter.consume(`${credentialKey}:auth`, RateLimits.auth);
Without this separation, the failure pattern is intermittent auth errors during peak usage periods, which RingCentral's support team will correctly identify as a token management problem, and which will require pausing the integration to fix.
3. Refresh tokens before expiry, not after. The token management guide explains that access tokens last an hour and refresh tokens last a week. What it doesn't say is that the worst time to discover a token has expired is mid-polling-cycle, when you're already consuming Medium and Heavy tier budget and can't afford urgent Auth retries. Refreshing when the token has five minutes remaining rather than zero keeps auth operations off the critical path:
function shouldRefresh(expireDate: string): boolean {
const fiveMinutesFromNow = Date.now() + 5 * 60 * 1000;
return new Date(expireDate).getTime() < fiveMinutesFromNow;
}
One more thing the docs don't mention: the per credential budget has to be coordinated, not just respected. Metering every tier correctly still fails if two processes meter independently against the same key, because each one is measuring its own consumption against a limit it does not own alone. A distributed lock keyed to the credential, held across one polling run and one download operation, is what actually stops them spending the same budget twice.
Gap 4: Page Size and Timeout Interact at Volume
The call log endpoint accepts up to 1000 records per page. The call log reference documents this as the maximum. What it doesn't document is that on large accounts with a wide time window, a 1000-record page request will sometimes time out before the response returns.
This is not a rate limit error, it's a connection timeout. The integration can't determine whether the data was returned or not. It either retries (burning more rate limit budget) or skips (dropping calls from that window permanently).
500 records per page with a short inter-page pause is the configuration that holds up in practice:
async function getAllCalls(opts: { dateFrom: string; dateTo: string; accessToken: string }) {
const results = [];
let page = 1;
while (true) {
if (page > 1) {
await new Promise((r) => setTimeout(r, 200));
}
const response = await fetchCallLogPage({ ...opts, perPage: 500, page });
results.push(...response.records);
if (response.records.length < 500) break;
page++;
}
return results;
}
This produces more requests than 1000 records per page would in theory. In practice, a half-completed 1000-record fetch that has to be retried costs more in rate limit budget than the additional pages from a smaller page size. And calls dropped from a missed window don't come back, they're gone from the processing history permanently.
Takeaway
A rate limit belongs to whatever it is counted against, and that is almost never the thing you can add more of.
That is the sentence I wish I'd had a week earlier, and it generalises well past RingCentral. Per-key, per-tenant, per-IP, per-account: whenever the budget is attached to something you cannot multiply, throughput stops being an engineering problem you can solve by scaling and becomes a scheduling problem you have to solve by coordinating. The instinct to add a worker is the instinct to make it worse.
The second thing worth carrying: when an API has separate limit tiers, it has separate failure modes, and they don't look alike. Read a tier table as a list of the different ways your integration can be broken while still reporting itself healthy. Three of RingCentral's four fail silently. Design the alerting around that, not around whether the process is running.
Where this doesn't apply: if you run exactly one process against one credential and your volume is comfortably inside the tier limits, most of this is theory and metering everything is overhead you don't need. It stops being theory the moment there are two of anything, and "two of anything" includes a polling trigger and a download action running side by side, which is easy not to count as two.
What I still don't know is how exact the documented numbers are. The docs give roughly 50 and roughly 40 for the Light and Medium tiers, and metering at about 85% has held across every account I've worked with, but I have never found a statement of the true ceiling or whether it shifts by plan tier. I treat the published figures as approximate on purpose, and I would rather leave headroom than find out precisely where the edge is on someone's busiest day.
Polling is also not the only architecture. If your use case tolerates it, webhook subscriptions change the detection model and the rate limit shape considerably. Different tradeoff, worth knowing it exists.
Before You Go Live
Three questions worth being able to answer:
- Are recording downloads de-duplicated to one per call, or one per participating agent?
- Are auth and data requests metered against separate counters, so a burst of one cannot starve the other?
- If two processes ever share a credential, what stops them spending the same budget twice?
If any of those is a shrug, that's where the calls are going. If you'd rather not spend the week I spent on the second one, book a discovery call with me.