linear-performance-tuning
Optimize Linear API performance by reducing N+1 queries, using efficient GraphQL patterns, and implementing caching.
Install
mkdir -p .claude/skills/linear-performance-tuning && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/7847" && unzip -o skill.zip -d .claude/skills/linear-performance-tuning && rm skill.zipInstalls to .claude/skills/linear-performance-tuning
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Optimize Linear API queries, caching, and batching for performance.Key capabilities
- →Flatten GraphQL queries to eliminate N+1 request patterns
- →Implement client-side caching for static data with TTLs
- →Batch multiple mutations into single GraphQL requests
- →Use webhooks for event-driven cache invalidation
- →Coalesce concurrent identical API requests
- →Perform incremental synchronization using updatedAt filters
How it works
The skill optimizes Linear API interactions by replacing lazy-loaded SDK relations with raw GraphQL queries, implementing local caching for static entities, and batching multiple mutations into single requests to stay within complexity budgets.
Inputs & outputs
When to use linear-performance-tuning
- →Fixing N+1 query patterns
- →Reducing Linear API request count
- →Implementing server-side caching
- →Optimizing GraphQL complexity budgets
About this skill
Linear Performance Tuning
Overview
Optimize Linear API usage for minimal latency and efficient resource consumption. The three main levers are: (1) query flattening to avoid N+1 and reduce complexity, (2) caching static data with webhook-driven invalidation, and (3) batching mutations into single GraphQL requests.
Key numbers:
- Query complexity budget: 250,000 pts/hour, max 10,000 per query
- Each property: 0.1 pt, each object: 1 pt, connections: multiply by
first - Best practice: sort by
updatedAtto get fresh data first
Prerequisites
- Working Linear integration with
@linear/sdk - Understanding of GraphQL query structure
- Optional: Redis for distributed caching
Instructions
Step 1: Eliminate N+1 Queries
The SDK lazy-loads relations. Accessing .assignee on 50 issues makes 50 separate API calls.
import { LinearClient } from "@linear/sdk";
const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });
// BAD: N+1 — 1 query for issues + 50 for assignees + 50 for states = 101 requests
const issues = await client.issues({ first: 50 });
for (const i of issues.nodes) {
const assignee = await i.assignee; // API call!
const state = await i.state; // API call!
console.log(`${i.identifier}: ${assignee?.name} [${state?.name}]`);
}
// GOOD: 1 request — use rawRequest with exact field selection
const response = await client.client.rawRequest(`
query TeamDashboard($teamId: String!) {
team(id: $teamId) {
issues(first: 50, orderBy: updatedAt) {
nodes {
id identifier title priority estimate updatedAt
assignee { name email }
state { name type }
labels { nodes { name color } }
project { name }
}
pageInfo { hasNextPage endCursor }
}
}
}
`, { teamId: "team-uuid" });
// Complexity: ~50 * (10 fields * 0.1 + 4 objects) = ~275 pts
Step 2: Cache Static Data
Teams, workflow states, and labels change rarely. Cache them with appropriate TTLs.
interface CacheEntry<T> {
data: T;
expiresAt: number;
}
class LinearCache {
private store = new Map<string, CacheEntry<any>>();
get<T>(key: string): T | null {
const entry = this.store.get(key);
if (!entry || Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}
return entry.data;
}
set<T>(key: string, data: T, ttlSeconds: number): void {
this.store.set(key, { data, expiresAt: Date.now() + ttlSeconds * 1000 });
}
invalidate(key: string): void {
this.store.delete(key);
}
}
const cache = new LinearCache();
// Teams: 10 minute TTL (almost never change)
async function getTeams(client: LinearClient) {
const cached = cache.get<any[]>("teams");
if (cached) return cached;
const teams = await client.teams();
cache.set("teams", teams.nodes, 600);
return teams.nodes;
}
// Workflow states: 30 minute TTL (rarely change)
async function getStates(client: LinearClient, teamId: string) {
const key = `states:${teamId}`;
const cached = cache.get<any[]>(key);
if (cached) return cached;
const team = await client.team(teamId);
const states = await team.states();
cache.set(key, states.nodes, 1800);
return states.nodes;
}
// Labels: 10 minute TTL
async function getLabels(client: LinearClient) {
const cached = cache.get<any[]>("labels");
if (cached) return cached;
const labels = await client.issueLabels();
cache.set("labels", labels.nodes, 600);
return labels.nodes;
}
Step 3: Webhook-Driven Cache Invalidation
Replace polling with webhooks. Invalidate cache when relevant entities change.
function handleCacheInvalidation(event: { type: string; action: string; data: any }) {
switch (event.type) {
case "Issue":
cache.invalidate(`issue:${event.data.id}`);
break;
case "WorkflowState":
cache.invalidate(`states:${event.data.teamId}`);
break;
case "IssueLabel":
cache.invalidate("labels");
break;
case "Team":
cache.invalidate("teams");
break;
}
}
Step 4: Batch Mutations
Combine multiple mutations into one GraphQL request.
// Instead of 100 separate updateIssue calls:
async function batchUpdatePriority(
client: LinearClient,
issueUpdates: Array<{ id: string; priority: number }>
) {
const chunkSize = 20; // Keep complexity manageable
for (let i = 0; i < issueUpdates.length; i += chunkSize) {
const chunk = issueUpdates.slice(i, i + chunkSize);
const mutations = chunk.map((u, j) =>
`u${j}: issueUpdate(id: "${u.id}", input: { priority: ${u.priority} }) { success }`
).join("\n");
await client.client.rawRequest(`mutation { ${mutations} }`);
}
}
// Batch issue creation
async function batchCreate(
client: LinearClient,
teamId: string,
issues: Array<{ title: string; priority?: number }>
) {
const mutations = issues.map((issue, i) =>
`c${i}: issueCreate(input: {
teamId: "${teamId}",
title: "${issue.title.replace(/"/g, '\\"')}",
priority: ${issue.priority ?? 3}
}) { success issue { id identifier } }`
).join("\n");
return client.client.rawRequest(`mutation { ${mutations} }`);
}
Step 5: Efficient Pagination
// Stream all issues without loading everything into memory
async function* paginateIssues(
client: LinearClient,
teamId: string,
pageSize = 50
) {
let cursor: string | undefined;
let hasNext = true;
while (hasNext) {
const result = await client.issues({
first: pageSize,
after: cursor,
filter: { team: { id: { eq: teamId } } },
orderBy: "updatedAt", // Fresh data first
});
yield result.nodes;
hasNext = result.pageInfo.hasNextPage;
cursor = result.pageInfo.endCursor;
}
}
// Process in batches
for await (const batch of paginateIssues(client, "team-uuid")) {
console.log(`Processing ${batch.length} issues`);
}
// Incremental sync: only fetch issues updated since last sync
const lastSync = "2026-03-20T00:00:00Z";
const updated = await client.issues({
first: 100,
filter: { updatedAt: { gte: lastSync } },
orderBy: "updatedAt",
});
Step 6: 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;
}
// Multiple components requesting same team data simultaneously = 1 API call
const team = await coalesce("team:ENG", () =>
client.teams({ filter: { key: { eq: "ENG" } } }).then(r => r.nodes[0])
);
Error Handling
| Error | Cause | Solution |
|---|---|---|
Query complexity too high | Deep nesting + large first | Use rawRequest() with flat fields, first: 50 |
| HTTP 429 | Burst exceeding rate budget | Add request queue with 100ms spacing |
| Stale cache | TTL too long | Shorten TTL or use webhook invalidation |
| Timeout | Query spanning too many records | Paginate with first: 50 + cursor |
Examples
Performance Benchmark
async function benchmark(label: string, fn: () => Promise<any>) {
const start = Date.now();
await fn();
console.log(`${label}: ${Date.now() - start}ms`);
}
await benchmark("Cold teams", () => client.teams());
await benchmark("Cached teams", () => getTeams(client));
await benchmark("50 issues (SDK)", () => client.issues({ first: 50 }));
await benchmark("50 issues (raw)", () => client.client.rawRequest(
`query { issues(first: 50) { nodes { id identifier title priority } } }`
));
Resources
When not to use it
- →When data requires real-time consistency without caching
- →When mutation volume is too low to benefit from batching
Prerequisites
Limitations
- →Query complexity is capped at 250,000 points per hour
- →Individual queries are limited to 10,000 complexity points
How it compares
Unlike standard SDK usage which triggers individual requests for related fields, this approach uses raw GraphQL queries to fetch all required data in a single round trip.
Compared to similar skills
linear-performance-tuning side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| linear-performance-tuning (this skill) | 1 | 27d | Review | Intermediate |
| nextjs-developer | 328 | 2mo | No flags | Advanced |
| sql-optimization-patterns | 64 | 2mo | No flags | Advanced |
| godot-gdscript-patterns | 57 | 4mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by jeremylongshore
View all by jeremylongshore →You might also like
nextjs-developer
zenobi-us
Expert Next.js developer mastering Next.js 14+ with App Router and full-stack features. Specializes in server components, server actions, performance optimization, and production deployment with focus on building fast, SEO-friendly applications.
sql-optimization-patterns
wshobson
Master SQL query optimization, indexing strategies, and EXPLAIN analysis to dramatically improve database performance and eliminate slow queries. Use when debugging slow queries, designing database schemas, or optimizing application performance.
godot-gdscript-patterns
sickn33
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices.
angular
sickn33
Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns. Use PROACTIVELY for Angular development, component architecture, state management, performance optimization, and migration to modern patterns.
core-web-vitals
davila7
Optimize Core Web Vitals (LCP, INP, CLS) for better page experience and search ranking. Use when asked to "improve Core Web Vitals", "fix LCP", "reduce CLS", "optimize INP", "page experience optimization", or "fix layout shifts".
chrome-devtools
mrgoonie
Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.