agentsclimarketplace

Clickup rate limits

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/clickup-pack/skills/clickup-rate-limits

'Handle ClickUp API rate limits with backoff, queuing, and header monitoring.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill clickup-rate-limits

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

5.8 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it

ClickUp Rate Limits

Overview

ClickUp enforces per-token, per-minute rate limits that vary by Workspace plan. When exceeded, the API returns HTTP 429 with rate limit headers.

Rate Limit Tiers

Workspace PlanRequests/Min/TokenBurst Support
Free Forever100No
Unlimited100No
Business100No
Business Plus1,000Yes
Enterprise10,000Yes

Rate Limit Headers

Every ClickUp API response includes these headers:

HeaderDescriptionExample
X-RateLimit-LimitMax requests in window100
X-RateLimit-RemainingRequests left in window95
X-RateLimit-ResetUnix timestamp when limit resets1695000060

Exponential Backoff with Jitter

async function clickupRequestWithRetry<T>(
  path: string,
  options: RequestInit = {},
  config = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 }
): Promise<T> {
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    const response = await fetch(`https://api.clickup.com/api/v2${path}`, {
      ...options,
      headers: {
        'Authorization': process.env.CLICKUP_API_TOKEN!,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    if (response.ok) return response.json();

    if (response.status === 429) {
      // Use server-provided reset time when available
      const resetTimestamp = response.headers.get('X-RateLimit-Reset');
      let waitMs: number;

      if (resetTimestamp) {
        waitMs = Math.max(0, parseInt(resetTimestamp) * 1000 - Date.now()) + 1000;
      } else {
        // Exponential backoff with jitter
        const exponential = config.baseDelayMs * Math.pow(2, attempt);
        const jitter = Math.random() * 1000;
        waitMs = Math.min(exponential + jitter, config.maxDelayMs);
      }

      console.warn(`Rate limited. Waiting ${(waitMs / 1000).toFixed(1)}s (attempt ${attempt + 1})`);
      await new Promise(r => setTimeout(r, waitMs));
      continue;
    }

    // Non-retryable errors
    if (response.status < 500 && response.status !== 429) {
      const error = await response.json().catch(() => ({}));
      throw new Error(`ClickUp ${response.status}: ${error.err ?? 'Unknown error'}`);
    }

    // Server errors: retry with backoff
    if (attempt < config.maxRetries) {
      const delay = config.baseDelayMs * Math.pow(2, attempt);
      await new Promise(r => setTimeout(r, delay));
    }
  }

  throw new Error(`ClickUp API: max retries exceeded for ${path}`);
}

Rate Limit Monitor

class ClickUpRateLimitMonitor {
  private remaining = 100;
  private limit = 100;
  private resetAt = 0;

  updateFromResponse(response: Response): void {
    const remaining = response.headers.get('X-RateLimit-Remaining');
    const limit = response.headers.get('X-RateLimit-Limit');
    const reset = response.headers.get('X-RateLimit-Reset');

    if (remaining) this.remaining = parseInt(remaining);
    if (limit) this.limit = parseInt(limit);
    if (reset) this.resetAt = parseInt(reset) * 1000;
  }

  shouldThrottle(): boolean {
    return this.remaining < 10 && Date.now() < this.resetAt;
  }

  getWaitMs(): number {
    return Math.max(0, this.resetAt - Date.now());
  }

  getUsagePercent(): number {
    return ((this.limit - this.remaining) / this.limit) * 100;
  }
}

Queue-Based Rate Limiting

import PQueue from 'p-queue';

// Stay under 100 req/min for Free/Unlimited/Business
const clickupQueue = new PQueue({
  concurrency: 5,        // Max parallel requests
  interval: 1000,        // Per second window
  intervalCap: 1,         // 1 request per second = 60/min (safe margin)
});

async function queuedClickUpRequest<T>(path: string, options?: RequestInit): Promise<T> {
  return clickupQueue.add(() => clickupRequestWithRetry(path, options));
}

// Bulk operations stay within limits automatically
const taskIds = ['abc', 'def', 'ghi', 'jkl'];
const tasks = await Promise.all(
  taskIds.map(id => queuedClickUpRequest(`/task/${id}`))
);

Pre-Flight Throttling

// Check headers before sending burst of requests
async function preFlightCheck(): Promise<{ safe: boolean; waitMs: number }> {
  const response = await fetch('https://api.clickup.com/api/v2/user', {
    headers: { 'Authorization': process.env.CLICKUP_API_TOKEN! },
  });

  const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '100');
  const reset = parseInt(response.headers.get('X-RateLimit-Reset') || '0') * 1000;

  if (remaining < 10) {
    return { safe: false, waitMs: Math.max(0, reset - Date.now()) };
  }
  return { safe: true, waitMs: 0 };
}

Error Handling

IssueCauseSolution
Constant 429sExceeding plan limitUpgrade plan or add request queuing
Thundering herdAll retries fire at same timeAdd random jitter to backoff
Missing reset headerOlder API versionFall back to exponential backoff
Burst rejectedToo many concurrentReduce concurrency in queue

Resources

Next Steps

For security configuration, see clickup-security-basics.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.