From clickup-pack
Optimizes ClickUp API rate limits and costs via caching, pagination, batching, webhooks, and plan comparisons. Reduces requests to avoid upgrades.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin clickup-packThis skill is limited to using the following tools:
ClickUp charges per-seat, not per-API-call. However, rate limits constrain throughput per plan tier. Optimizing API usage means reducing request count to stay within rate limits and avoid needing plan upgrades.
Optimizes ClickUp API v2 performance with TypeScript patterns for caching, pagination via async generators, and Node.js connection pooling to reduce latency in task/space queries.
Optimizes Linear API usage: audit requests/complexity, replace polling with webhooks, minimize query complexity to respect rate limits.
Optimizes Attio API costs: audit usage with trackers, reduce requests via caching, select plans, monitor limits, and set budget alerts.
Share bugs, ideas, or general feedback.
ClickUp charges per-seat, not per-API-call. However, rate limits constrain throughput per plan tier. Optimizing API usage means reducing request count to stay within rate limits and avoid needing plan upgrades.
| Plan | Price | Rate Limit | Key API Features |
|---|---|---|---|
| Free Forever | $0 | 100 req/min | Full API access, 100 uses of automations |
| Unlimited | $7/member | 100 req/min | Unlimited storage, integrations |
| Business | $12/member | 100 req/min | Custom fields, time tracking, goals |
| Business Plus | $19/member | 1,000 req/min | Custom role creation, admin training |
| Enterprise | Custom | 10,000 req/min | SSO/SAML, advanced permissions, dedicated support |
Spaces, folders, and lists change rarely. Cache them aggressively.
import { LRUCache } from 'lru-cache';
const structureCache = new LRUCache<string, any>({
max: 500,
ttl: 300000, // 5 minutes for hierarchy data
});
async function getCachedSpaces(teamId: string) {
const key = `spaces:${teamId}`;
let spaces = structureCache.get(key);
if (!spaces) {
const data = await clickupRequest(`/team/${teamId}/space?archived=false`);
spaces = data.spaces;
structureCache.set(key, spaces);
}
return spaces;
}
Get Tasks returns max 100 per page. Fetch only what you need.
// Bad: fetch all pages when you only need recent tasks
// Good: use filters to minimize pages
async function getRecentTasks(listId: string, limit = 25) {
return clickupRequest(`/list/${listId}/task?${new URLSearchParams({
page: '0',
order_by: 'updated',
reverse: 'true',
subtasks: 'true',
include_closed: 'false',
})}`);
}
Set custom fields during task creation instead of separate calls.
// Bad: 3 API calls (create + 2 custom field updates)
const task = await createTask(listId, { name: 'Task' });
await setCustomField(task.id, field1Id, value1);
await setCustomField(task.id, field2Id, value2);
// Good: 1 API call (custom fields in create body)
await createTask(listId, {
name: 'Task',
custom_fields: [
{ id: field1Id, value: value1 },
{ id: field2Id, value: value2 },
],
});
// Bad: poll every 30 seconds (2 req/min wasted)
setInterval(() => checkForUpdates(), 30000);
// Good: register webhook, process events on-demand (0 polling requests)
await clickupRequest(`/team/${teamId}/webhook`, {
method: 'POST',
body: JSON.stringify({
endpoint: 'https://myapp.com/webhooks/clickup',
events: ['taskUpdated', 'taskCreated'],
}),
});
class ClickUpUsageTracker {
private requestLog: Array<{ timestamp: number; endpoint: string }> = [];
track(endpoint: string): void {
this.requestLog.push({ timestamp: Date.now(), endpoint });
// Keep only last hour
const cutoff = Date.now() - 3600000;
this.requestLog = this.requestLog.filter(r => r.timestamp > cutoff);
}
getRequestsPerMinute(): number {
const oneMinAgo = Date.now() - 60000;
return this.requestLog.filter(r => r.timestamp > oneMinAgo).length;
}
getTopEndpoints(n = 5): Array<{ endpoint: string; count: number }> {
const counts = new Map<string, number>();
for (const r of this.requestLog) {
counts.set(r.endpoint, (counts.get(r.endpoint) ?? 0) + 1);
}
return [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.map(([endpoint, count]) => ({ endpoint, count }));
}
needsUpgrade(): boolean {
return this.getRequestsPerMinute() > 80; // 80% of Free tier limit
}
}
| Monthly Requests | Recommended Plan | Rationale |
|---|---|---|
| < 144,000 | Free Forever | 100/min * 60min * 24h = 144K/day max |
| 100-1000 req/min sustained | Business Plus | 10x rate limit increase |
| > 1000 req/min sustained | Enterprise | 10,000 req/min + dedicated support |
| Issue | Cause | Solution |
|---|---|---|
| Constant 429 errors | Hit rate ceiling | Implement queuing or upgrade |
| Cache stale data | TTL too long | Invalidate via webhooks |
| Redundant API calls | No deduplication | Use DataLoader batching |
| Polling overhead | No webhook setup | Switch to event-driven |
For architecture patterns, see clickup-reference-architecture.