Skill 132 · Build Zoom REST API App
Subchapter 132.25
references/rate-limits.mdMarkdown10 KBView on GitHub
Understanding and handling Zoom API rate limits for reliable integrations.
Zoom APIs enforce rate limits to ensure fair usage. Limits vary by endpoint category, account type, and are shared across all apps on an account.
| Category | Free | Pro | Business+ |
|---|---|---|---|
| Light | 4/sec, 6,000/day | 30/sec | 80/sec |
| Medium | 2/sec, 2,000/day | 20/sec | 60/sec |
| Heavy | 1/sec, 1,000/day | 10/sec* | 40/sec* |
| Resource-Intensive | 10/min, 30,000/day | 10/min* | 20/min* |
* Daily limits (shared):
| Category | Pro | Business+ |
|---|---|---|
| Light | 20/sec | 40/sec |
| Medium | 10/sec | 20/sec |
| Heavy | 5/sec, 15,000/day* | 10/sec, 30,000/day* |
| Resource-Intensive | 5/min, 15,000/day* | 10/min, 30,000/day* |
| Category | Pro | Business+ |
|---|---|---|
| Light | 20/sec | 40/sec |
| Medium | 10/sec | 20/sec |
| Heavy | 5/sec, 15,000/day* | 10/sec, 30,000/day* |
* Daily limit shared with Resource-Intensive APIs
| Light | Medium | Heavy |
|---|---|---|
| Add Meeting Registrant | Create Meeting | Get Daily Usage Report |
| Get A Meeting | Get Past Meeting Participants | List Devices |
| Get Meeting Recordings | List All Recordings | |
| Update A Meeting | List Meetings | |
| Delete Meeting Recordings | List Webinars |
| Operation | Limit | Reset |
|---|---|---|
| Meeting/Webinar Create/Update | 100/day per user | 00:00 UTC |
| Registrant Addition | 3/day per registrant | 00:00 UTC |
| Registrant Status Updates | 10/day per registrant | 00:00 UTC |
Note: The 100/day limit applies to all Meeting/Webinar IDs hosted by a specific user.
Zoom enforces lock-key limits for user resource operations:
| Scenario | Behavior |
|---|---|
| Multiple DELETE on same userId | Only 1 concurrent DELETE allowed |
POST to /v2/users | Blocks GET/PATCH/PUT/DELETE until complete |
Error Response:
{
"code": 429,
"message": "Too many concurrent requests. A request to disassociate this user has already been made."
}Every API response includes rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Category | Light, Medium, Heavy, or Resource-intensive |
X-RateLimit-Type | QPS (per-second) or Daily-limit |
X-RateLimit-Limit | Maximum requests in current window |
X-RateLimit-Remaining | Requests remaining in current window |
| Header | Description |
|---|---|
X-RateLimit-Reset | Unix timestamp when limit resets |
| Header | Description |
|---|---|
Retry-After | ISO8601 datetime when you can retry |
Normal response:
X-RateLimit-Category: Medium
X-RateLimit-Type: QPS
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55Rate limited (per-second):
HTTP/1.1 429 Too Many Requests
X-RateLimit-Category: Light
X-RateLimit-Type: QPS
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800Rate limited (daily):
HTTP/1.1 429 Too Many Requests
X-RateLimit-Category: Heavy
X-RateLimit-Type: Daily-limit
X-RateLimit-Limit: 60000
X-RateLimit-Remaining: 0
Retry-After: 2025-01-20T00:00:00Z{
"code": 429,
"message": "You have reached the maximum per-second rate limit for this API. Try again later."
}{
"code": 429,
"message": "You have reached the maximum daily rate limit for this API. Refer to the response header for details on when you can make another request."
}async function callZoomAPI(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
// Check for Retry-After (daily limit)
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
const retryDate = new Date(retryAfter);
const waitMs = retryDate - Date.now();
console.log(`Daily limit hit. Retry after: ${retryAfter}`);
await sleep(waitMs);
continue;
}
// Per-second limit - use exponential backoff
const delay = Math.pow(2, attempt) * 1000;
const jitter = delay * 0.2 * Math.random(); // 20% jitter
console.log(`Rate limited. Retrying in ${delay + jitter}ms`);
await sleep(delay + jitter);
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}async function callAPIWithMonitoring(url, options) {
const response = await fetch(url, options);
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const limit = parseInt(response.headers.get('X-RateLimit-Limit'));
const category = response.headers.get('X-RateLimit-Category');
const type = response.headers.get('X-RateLimit-Type');
console.log(`[${category}/${type}] ${remaining}/${limit} remaining`);
// Proactive throttling
if (remaining < limit * 0.1) { // Less than 10% remaining
console.warn('Approaching rate limit - throttling requests');
await sleep(1000); // Slow down
}
return response;
}For high-volume applications:
class RateLimitedQueue {
constructor(maxConcurrent = 10, minDelayMs = 100) {
this.queue = [];
this.running = 0;
this.maxConcurrent = maxConcurrent;
this.minDelayMs = minDelayMs;
}
async add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}
const { requestFn, resolve, reject } = this.queue.shift();
this.running++;
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
await sleep(this.minDelayMs);
this.process();
}
}
}
// Usage
const queue = new RateLimitedQueue(10, 100); // 10 concurrent, 100ms min delay
const results = await Promise.all([
queue.add(() => fetch('/api/users/1')),
queue.add(() => fetch('/api/users/2')),
queue.add(() => fetch('/api/users/3')),
// ... more requests
]);const cache = new Map();
const CACHE_TTL = 60000; // 1 minute
async function cachedGet(url, options) {
const cached = cache.get(url);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch(url, options);
const data = await response.json();
cache.set(url, { data, timestamp: Date.now() });
return data;
}Instead of polling for changes:
// DON'T: Poll every minute
setInterval(async () => {
const meetings = await getMeetings(); // Uses API quota
}, 60000);Use webhooks:
// DO: Receive webhook events
app.post('/webhook', (req, res) => {
const event = req.body;
if (event.event === 'meeting.started') {
handleMeetingStarted(event.payload);
}
res.status(200).send();
});Use list endpoints with pagination instead of individual fetches:
// DON'T: Fetch users one by one
for (const userId of userIds) {
const user = await getUser(userId); // N API calls
}
// DO: Fetch users in batches
const users = await listUsers({ page_size: 300 }); // 1 API callFor Quality of Service data, use QSS (push-based) instead of polling:
// DON'T: Burst all requests at once
await Promise.all(users.map(u => updateUser(u))); // May hit rate limit
// DO: Distribute over time
for (const user of users) {
await updateUser(user);
await sleep(100); // 100ms between requests
}| Plan | Uses Limits |
|---|---|
| Pay As You Go (Deprecated) | Pro |
| Annual Prepay Monthly Usage | Pro |
| All other plans | Business+ |
| Issue | Solution |
|---|---|
| 429 on first request of the day | Another app on account used quota |
| Different limits than documented | Check account type (Free/Pro/Business+) |
| Meeting create fails at 100/day | Per-user limit - use different host |
| Concurrent DELETE errors | Serialize DELETE operations on same user |
| Daily limit hit unexpectedly | Heavy + Resource-Intensive share quota |