Files
2026-07-13 12:20:06 +08:00

9.9 KiB

REST API - Rate Limits

Understanding and handling Zoom API rate limits for reliable integrations.

Overview

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.

Rate Limit Categories

Main REST API

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):

  • Pro: 30,000/day (Heavy + Resource-Intensive combined)
  • Business+: 60,000/day (Heavy + Resource-Intensive combined)

Zoom Phone API

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*

Zoom Contact Center API

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

Endpoint Category Examples

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

Special Per-User Limits

Meeting/Webinar Operations

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.

Lock-Key Limits (Concurrent Operations)

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."
}

Response Headers

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

On Per-Second/Minute Limit Hit

Header Description
X-RateLimit-Reset Unix timestamp when limit resets

On Daily Limit Hit

Header Description
Retry-After ISO8601 datetime when you can retry

Example Headers

Normal response:

X-RateLimit-Category: Medium
X-RateLimit-Type: QPS
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55

Rate 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: 1705312800

Rate 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

Error Responses

Per-Second Limit

{
  "code": 429,
  "message": "You have reached the maximum per-second rate limit for this API. Try again later."
}

Daily Limit

{
  "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."
}

Handling Rate Limits

Basic Retry with Backoff

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');
}

Monitor Rate Limit Headers

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;
}

Request Queue Pattern

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
]);

Best Practices

1. Cache GET Responses

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;
}

2. Use Webhooks Instead of Polling

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();
});

3. Batch Operations

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 call

4. Use QSS for Quality Data

For Quality of Service data, use QSS (push-based) instead of polling:

  • QSS streams telemetry via webhooks/WebSocket
  • Pushes data 4-6 times per minute
  • Reduces need for Reports API polling

5. Distribute Requests Over Time

// 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
}

Account Type Notes

Rate Limits Are Per-Account

  • Limits are shared by ALL users and ALL apps on the account
  • Upgrading account increases limits for everyone
  • Not per-app - one heavy app can impact others

Business+ Includes

  • Business
  • Education
  • Enterprise
  • Partners

Video SDK Accounts

Plan Uses Limits
Pay As You Go (Deprecated) Pro
Annual Prepay Monthly Usage Pro
All other plans Business+

Event Subscription Limits

  • Maximum 10 event subscriptions per application
  • No limit on events per subscription
  • Event subscription API has Heavy rate limit
  • WebSocket: Only 1 subscription connection at a time (new connection closes previous)

Common Gotchas

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

Resources