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".
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.
| 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 |
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();
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 });
});
// 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 } }
}
}`;
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];
});
}
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
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);
}
// 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;
}
rawRequest() for exact field selectionupdatedAt for incremental sync| 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 |