Skillshub clickup-rate-limits

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/clickup-rate-limits" ~/.claude/skills/comeonoliver-skillshub-clickup-rate-limits && rm -rf "$T"
manifest: skills/jeremylongshore/claude-code-plugins-plus-skills/clickup-rate-limits/SKILL.md
source content

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-Limit
Max requests in window
100
X-RateLimit-Remaining
Requests left in window
95
X-RateLimit-Reset
Unix timestamp when limit resets
1695000060

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
.