--- name: linear-cost-tuning description: 'Optimize Linear API usage, reduce unnecessary calls, and maximize efficiency within rate limit budgets. Trigger: "linear cost", "reduce linear API calls", "linear efficiency", "linear API usage", "optimize linear costs", "linear budget". ' allowed-tools: Read, Write, Edit, Grep version: 1.12.0 license: MIT author: Jeremy Longshore tags: - saas - linear - api - cost-optimization compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw --- # Linear Cost Tuning ## Overview Optimize Linear API usage to stay within rate budgets and minimize infrastructure costs. Linear's API is free (no per-request billing), but rate limits (5,000 requests/hour, 250,000 complexity/hour) constrain throughput. Efficient patterns let you do more within these limits. ## Cost Factors | Factor | Budget Impact | Optimization | |--------|--------------|-------------| | Request count | 5,000/hr limit | Batch operations, coalesce requests | | Query complexity | 250,000/hr limit | Flat queries, small page sizes | | Payload size | Bandwidth + latency | Select only needed fields | | Polling frequency | Wastes budget | Replace with webhooks | | Webhook volume | Processing costs | Filter by event type and team | ## Instructions ### Step 1: Audit Current Usage ```typescript import { LinearClient } from "@linear/sdk"; class UsageTracker { private requests = 0; private totalComplexity = 0; private startTime = Date.now(); track(complexity: number) { this.requests++; this.totalComplexity += complexity; } report() { const elapsedHours = (Date.now() - this.startTime) / 3600000; return { requests: this.requests, requestsPerHour: Math.round(this.requests / elapsedHours), totalComplexity: this.totalComplexity, complexityPerHour: Math.round(this.totalComplexity / elapsedHours), budgetUsed: { requests: `${Math.round((this.requests / elapsedHours / 5000) * 100)}%`, complexity: `${Math.round((this.totalComplexity / elapsedHours / 250000) * 100)}%`, }, }; } } const tracker = new UsageTracker(); ``` ### Step 2: Replace Polling with Webhooks The single biggest optimization. A polling loop checking every minute uses 1,440 requests/day. A webhook uses zero. ```typescript // BAD: Polling every 60 seconds (1,440 req/day, ~60 req/hr) setInterval(async () => { const issues = await client.issues({ first: 100, filter: { updatedAt: { gte: lastCheck } }, }); await syncIssues(issues.nodes); lastCheck = new Date().toISOString(); }, 60000); // GOOD: Webhook receives updates in real-time (0 requests for monitoring) app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => { // Verify signature, process event const event = JSON.parse(req.body.toString()); if (event.type === "Issue") { syncSingleIssue(event.data); } res.json({ ok: true }); }); ``` ### Step 3: Minimize Query Complexity ```typescript // BAD: ~12,500 pts — deeply nested with large page // issues(50) * (labels(50 default) * fields + comments(50) * user) const expensive = `query { issues(first: 50) { nodes { id title assignee { name } labels { nodes { name } } comments(first: 10) { nodes { body user { name } } } } } }`; // GOOD: ~55 pts — flat fields only const cheap = `query { issues(first: 50) { nodes { id identifier title priority estimate } } }`; // Fetch relations separately only when needed const issueDetail = `query($id: String!) { issue(id: $id) { id identifier title description priority assignee { name email } state { name type } labels { nodes { name color } } } }`; ``` ### Step 4: Request Coalescing Deduplicate concurrent identical requests. ```typescript const inflight = new Map>(); async function coalesce(key: string, fn: () => Promise): Promise { if (inflight.has(key)) return inflight.get(key)!; const promise = fn().finally(() => inflight.delete(key)); inflight.set(key, promise); return promise; } // 10 concurrent requests for same team = 1 actual API call async function getTeam(teamKey: string) { return coalesce(`team:${teamKey}`, async () => { const result = await client.teams({ filter: { key: { eq: teamKey } } }); return result.nodes[0]; }); } ``` ### Step 5: Cache with Smart TTLs ```typescript const CACHE_TTLS = { teams: 600, // 10 min — teams almost never change workflowStates: 1800, // 30 min — states rarely change labels: 600, // 10 min — labels rarely change issues: 60, // 1 min — issues change frequently viewer: 3600, // 1 hr — your identity doesn't change }; // Combined with webhook invalidation, even short TTLs // dramatically reduce redundant requests ``` ### Step 6: Filter Webhook Events Skip irrelevant events to reduce processing costs. ```typescript async function processEvent(event: any): Promise { // Skip bot/automation events to avoid loops if (event.actor?.type === "application") return; // Skip trivial field updates (e.g., sortOrder changes) if (event.type === "Issue" && event.action === "update") { const significantFields = ["stateId", "assigneeId", "priority", "title"]; const changedFields = Object.keys(event.updatedFrom ?? {}); if (!changedFields.some(f => significantFields.includes(f))) return; } // Skip specific teams if not relevant const relevantTeamKeys = ["ENG", "PRODUCT"]; if (event.data?.team?.key && !relevantTeamKeys.includes(event.data.team.key)) return; // Process significant event await handleEvent(event); } ``` ### Step 7: Incremental Sync Pattern ```typescript // Instead of fetching ALL issues every sync: // Sort by updatedAt, stop when you reach already-synced data async function incrementalSync(client: LinearClient, lastSyncTime: string) { let cursor: string | undefined; let synced = 0; while (true) { const issues = await client.issues({ first: 100, after: cursor, filter: { updatedAt: { gte: lastSyncTime } }, orderBy: "updatedAt", }); for (const issue of issues.nodes) { await upsertLocally(issue); synced++; } if (!issues.pageInfo.hasNextPage) break; cursor = issues.pageInfo.endCursor; } console.log(`Synced ${synced} issues since ${lastSyncTime}`); return synced; } ``` ## Optimization Checklist - [ ] Replace all polling with webhooks - [ ] Implement request caching (static data: 10-30 min TTL) - [ ] Add request coalescing for concurrent identical calls - [ ] Filter webhook events (skip bots, trivial updates, irrelevant teams) - [ ] Keep query complexity under 500 pts per query - [ ] Use `rawRequest()` for exact field selection - [ ] Sort by `updatedAt` for incremental sync - [ ] Batch mutations (20 per GraphQL request) - [ ] Cache teams/states/labels with webhook invalidation ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | Rate limit hit frequently | Too many requests | Implement coalescing + caching | | Stale cache data | TTL too long | Use webhook-driven invalidation | | High complexity queries | Nested relations | Flatten with `rawRequest()`, fetch relations lazily | | Webhook processing overload | Unfiltered events | Add type/team/field filtering | ## Resources - [Linear Rate Limiting](https://linear.app/developers/rate-limiting) - [Linear Best Practices](https://linear.app/developers/graphql) - [Webhooks](https://linear.app/developers/webhooks)