Agent Skills: Linear Cost Tuning

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/linear-cost-tuning

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/linear-pack/skills/linear-cost-tuning

Skill Files

Browse the full folder contents for linear-cost-tuning.

Download Skill

Loading file tree…

plugins/saas-packs/linear-pack/skills/linear-cost-tuning/SKILL.md

Skill Metadata

Name
linear-cost-tuning
Description
|

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

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.

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

// 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.

const inflight = new Map<string, Promise<any>>();

async function coalesce<T>(key: string, fn: () => Promise<T>): Promise<T> {
  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

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.

async function processEvent(event: any): Promise<void> {
  // 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

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