Skip to main content
AI/MLjeremylongshore

clickup-cost-tuning

'Optimize ClickUp API usage costs through plan selection, request reduction,

Stars
2,267
Source
jeremylongshore/claude-code-plugins-plus-skills
Updated
2026-05-31
Slug
jeremylongshore--claude-code-plugins-plus-skills--clickup-cost-tuning
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/HEAD/plugins/saas-packs/clickup-pack/skills/clickup-cost-tuning/SKILL.md -o .claude/skills/clickup-cost-tuning.md

Drops the SKILL.md into .claude/skills/clickup-cost-tuning.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

ClickUp Cost Tuning

Overview

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.

ClickUp Pricing (Per Member/Month)

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

Request Reduction Strategies

1. Cache Workspace Structure

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

2. Use Pagination Efficiently

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

3. Batch with Custom Fields

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

4. Use Webhooks Instead of Polling

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

Usage Monitoring

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

Cost Decision Matrix

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

Error Handling

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

Prerequisites

  • Observed request, cache, webhook, and rate-limit baseline by workload
  • Approved data-freshness, reliability, and cost/plan decision owner
  • Staging workload capable of measuring a proposed change safely

Instructions

Measure redundant calls, poll cadence, cache hits, and quota headroom before changing a plan, concurrency, or caching policy. Prefer correct event-driven invalidation and batching over a higher limit; evaluate one change at a time and retain the last verified configuration for rollback.

Output

Publish a capacity/cost decision with baseline, proposed control, expected and observed request rate, freshness/error impact, owner approval, and rollback trigger. Pricing or quota estimates are not guarantees and must not include private task content or credentials.

Examples

Replace an hourly polling job in staging with a webhook-driven invalidation path, compare request count and freshness for a week, then decide whether the existing plan remains sufficient. If 429s or stale data increase, restore the prior policy and correct the event path before changing plan tier.

Resources

Next Steps

For architecture patterns, see clickup-reference-architecture.