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 Plan | Requests/Min/Token | Burst Support | |----------------|-------------------|---------------| | Free Forever | 100 | No | | Unlimited | 100 | No | | Business | 100 | No | | Business Plus | 1,000 | Yes | | Enterprise | 10,000 | Yes |
Rate Limit Headers
Every ClickUp API response includes these headers:
| Header | Description | Example |
|--------|-------------|---------|
| 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
| Issue | Cause | Solution |
|-------|-------|----------|
| Constant 429s | Exceeding plan limit | Upgrade plan or add request queuing |
| Thundering herd | All retries fire at same time | Add random jitter to backoff |
| Missing reset header | Older API version | Fall back to exponential backoff |
| Burst rejected | Too many concurrent | Reduce concurrency in queue |
Resources
Next Steps
For security configuration, see clickup-security-basics.