linear-rate-limits
Guidance on understanding Linear's complexity-based rate limiting and managing request quotas effectively.
Install
mkdir -p .claude/skills/linear-rate-limits && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/9131" && unzip -o skill.zip -d .claude/skills/linear-rate-limits && rm skill.zipInstalls to .claude/skills/linear-rate-limits
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.
Handle Linear API rate limiting, complexity budgets, and quotas.Key capabilities
- →Read Linear API rate limit headers from responses
- →Implement exponential backoff with jitter for retries
- →Space out requests using a token bucket queue
- →Reduce query complexity by selecting minimal fields
- →Batch multiple mutations into a single GraphQL request
- →Monitor remaining requests and complexity budgets
How it works
The skill provides code examples to read rate limit headers, implement exponential backoff for retries, and use a token bucket queue to space out requests. It also demonstrates how to reduce query complexity by selecting fewer fields and batching mutations to stay within Linear's leaky bucket rate limits.
Inputs & outputs
When to use linear-rate-limits
- →Handle rate limit response headers
- →Optimize API query complexity
- →Avoid 429 API errors
- →Monitor API request quotas
About this skill
Linear Rate Limits
Overview
Linear uses the leaky bucket algorithm with two rate limiting dimensions. Understanding both is critical for reliable integrations:
| Budget | Limit | Refill Rate |
|---|---|---|
| Requests | 5,000/hour per API key | ~83/min constant refill |
| Complexity | 250,000 points/hour | ~4,167/min constant refill |
| Max single query | 10,000 points | Hard reject if exceeded |
Complexity scoring: Each property = 0.1 pt, each object = 1 pt, connections multiply children by first arg (default 50), then round up.
Prerequisites
@linear/sdkinstalled- Understanding of HTTP response headers
- Familiarity with async/await patterns
Instructions
Step 1: Read Rate Limit Headers
Linear returns rate limit info on every response.
const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
Authorization: process.env.LINEAR_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "{ viewer { id } }" }),
});
// Key headers
const headers = {
requestsRemaining: response.headers.get("x-ratelimit-requests-remaining"),
requestsLimit: response.headers.get("x-ratelimit-requests-limit"),
requestsReset: response.headers.get("x-ratelimit-requests-reset"),
complexityRemaining: response.headers.get("x-ratelimit-complexity-remaining"),
complexityLimit: response.headers.get("x-ratelimit-complexity-limit"),
queryComplexity: response.headers.get("x-complexity"),
};
console.log(`Requests: ${headers.requestsRemaining}/${headers.requestsLimit}`);
console.log(`Complexity: ${headers.complexityRemaining}/${headers.complexityLimit}`);
console.log(`This query cost: ${headers.queryComplexity} points`);
Step 2: Exponential Backoff with Jitter
import { LinearClient } from "@linear/sdk";
class RateLimitedClient {
private client: LinearClient;
constructor(apiKey: string) {
this.client = new LinearClient({ apiKey });
}
async withRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const isRateLimited = error.status === 429 ||
error.message?.includes("rate") ||
error.type === "ratelimited";
if (!isRateLimited || attempt === maxRetries - 1) throw error;
// Exponential backoff: 1s, 2s, 4s, 8s, 16s + jitter
const delay = 1000 * Math.pow(2, attempt) + Math.random() * 500;
console.warn(`Rate limited (attempt ${attempt + 1}/${maxRetries}), waiting ${Math.round(delay)}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}
get sdk() { return this.client; }
}
Step 3: Request Queue with Token Bucket
Prevent bursts by spacing requests evenly.
class RequestQueue {
private queue: Array<{ fn: () => Promise<any>; resolve: Function; reject: Function }> = [];
private processing = false;
private intervalMs: number;
constructor(requestsPerSecond = 10) {
this.intervalMs = 1000 / requestsPerSecond;
}
async enqueue<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
if (!this.processing) this.processQueue();
});
}
private async processQueue() {
this.processing = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift()!;
try {
resolve(await fn());
} catch (error) {
reject(error);
}
if (this.queue.length > 0) {
await new Promise(r => setTimeout(r, this.intervalMs));
}
}
this.processing = false;
}
}
// Usage: 8 requests/second max
const queue = new RequestQueue(8);
const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });
const teamResults = await Promise.all(
teamIds.map(id => queue.enqueue(() => client.team(id)))
);
Step 4: Reduce Query Complexity
// HIGH COMPLEXITY (~12,500 pts):
// 250 issues * (1 issue + 50 labels * 0.1 per field) = expensive
// const heavy = await client.issues({ first: 250 });
// LOW COMPLEXITY (~55 pts):
// 50 issues * (5 fields * 0.1 + 1 object) = cheap
const light = await client.issues({
first: 50,
filter: { team: { id: { eq: teamId } } },
});
// Use rawRequest for minimal field selection
const minimal = await client.client.rawRequest(`
query { issues(first: 50) { nodes { id identifier title priority } } }
`);
// Sort by updatedAt to get fresh data first, avoid paginating everything
const fresh = await client.issues({
first: 50,
orderBy: "updatedAt",
filter: { updatedAt: { gte: lastSyncTime } },
});
Step 5: Batch Mutations
Combine multiple mutations into one GraphQL request.
// Instead of 100 separate issueUpdate calls (~100 requests):
async function batchUpdatePriority(client: LinearClient, issueIds: string[], priority: number) {
const chunkSize = 20; // Keep each batch under complexity limit
for (let i = 0; i < issueIds.length; i += chunkSize) {
const chunk = issueIds.slice(i, i + chunkSize);
const mutations = chunk.map((id, j) =>
`u${j}: issueUpdate(id: "${id}", input: { priority: ${priority} }) { success }`
).join("\n");
await queue.enqueue(() =>
client.client.rawRequest(`mutation BatchUpdate { ${mutations} }`)
);
}
}
// Batch archive
async function batchArchive(client: LinearClient, issueIds: string[]) {
for (let i = 0; i < issueIds.length; i += 20) {
const chunk = issueIds.slice(i, i + 20);
const mutations = chunk.map((id, j) =>
`a${j}: issueArchive(id: "${id}") { success }`
).join("\n");
await client.client.rawRequest(`mutation { ${mutations} }`);
}
}
Step 6: Rate Limit Monitor
class RateLimitMonitor {
private remaining = { requests: 5000, complexity: 250000 };
update(headers: Headers) {
const reqRemaining = headers.get("x-ratelimit-requests-remaining");
const cxRemaining = headers.get("x-ratelimit-complexity-remaining");
if (reqRemaining) this.remaining.requests = parseInt(reqRemaining);
if (cxRemaining) this.remaining.complexity = parseInt(cxRemaining);
}
isLow(): boolean {
return this.remaining.requests < 100 || this.remaining.complexity < 5000;
}
getStatus() {
return {
requests: this.remaining.requests,
complexity: this.remaining.complexity,
healthy: !this.isLow(),
};
}
}
Error Handling
| Error | Cause | Solution |
|---|---|---|
| HTTP 429 | Request or complexity budget exceeded | Parse headers, back off exponentially |
Query complexity too high | Single query > 10,000 pts | Reduce first to 50, remove nested relations |
| Burst of 429s on startup | Init fetches too much data | Stagger startup queries, cache static data |
| Timeout on SDK call | Server under load | Add 30s timeout, retry once |
Examples
Rate Limit Status Check
curl -s -I -X POST https://api.linear.app/graphql \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ viewer { id } }"}' 2>&1 | grep -i ratelimit
Safe Bulk Import
const rlClient = new RateLimitedClient(process.env.LINEAR_API_KEY!);
const items = [/* issues to import */];
for (let i = 0; i < items.length; i++) {
await rlClient.withRetry(() =>
rlClient.sdk.createIssue({ teamId: "team-uuid", title: items[i].title })
);
if ((i + 1) % 50 === 0) console.log(`Imported ${i + 1}/${items.length}`);
}
Resources
When not to use it
- →When the issue is not related to Linear API rate limiting or quotas
- →When not dealing with HTTP 429 errors
- →When not optimizing API usage patterns for Linear
Prerequisites
Limitations
- →A single query exceeding 10,000 complexity points will be hard rejected
- →A burst of 429s can occur on startup if initial fetches request too much data
- →SDK calls may timeout under server load
How it compares
This skill provides specific code implementations and strategies for handling Linear's unique leaky bucket rate limiting and complexity budgets, offering more targeted solutions than generic API rate limit handling.
Compared to similar skills
linear-rate-limits side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| linear-rate-limits (this skill) | 0 | 27d | Caution | Intermediate |
| groq-performance-tuning | 1 | 27d | No flags | Intermediate |
| openrouter-streaming-setup | 1 | 27d | Review | Intermediate |
| ideogram-rate-limits | 2 | 27d | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by jeremylongshore
View all by jeremylongshore →You might also like
groq-performance-tuning
jeremylongshore
Optimize Groq API performance with caching, batching, and connection pooling. Use when experiencing slow API responses, implementing caching strategies, or optimizing request throughput for Groq integrations. Trigger with phrases like "groq performance", "optimize groq", "groq latency", "groq caching", "groq slow", "groq batch".
openrouter-streaming-setup
jeremylongshore
Implement streaming responses with OpenRouter. Use when building real-time chat interfaces or reducing time-to-first-token. Trigger with phrases like 'openrouter streaming', 'openrouter sse', 'stream response', 'real-time openrouter'.
ideogram-rate-limits
jeremylongshore
Implement Ideogram rate limiting, backoff, and idempotency patterns. Use when handling rate limit errors, implementing retry logic, or optimizing API request throughput for Ideogram. Trigger with phrases like "ideogram rate limit", "ideogram throttling", "ideogram 429", "ideogram retry", "ideogram backoff".
firecrawl-reliability-patterns
jeremylongshore
Implement FireCrawl reliability patterns including circuit breakers, idempotency, and graceful degradation. Use when building fault-tolerant FireCrawl integrations, implementing retry strategies, or adding resilience to production FireCrawl services. Trigger with phrases like "firecrawl reliability", "firecrawl circuit breaker", "firecrawl idempotent", "firecrawl resilience", "firecrawl fallback", "firecrawl bulkhead".
generating-grpc-services
jeremylongshore
Generate gRPC service definitions, stubs, and implementations from Protocol Buffers. Use when creating high-performance gRPC services. Trigger with phrases like "generate gRPC service", "create gRPC API", or "build gRPC server".
perplexity-performance-tuning
jeremylongshore
Optimize Perplexity API performance with caching, batching, and connection pooling. Use when experiencing slow API responses, implementing caching strategies, or optimizing request throughput for Perplexity integrations. Trigger with phrases like "perplexity performance", "optimize perplexity", "perplexity latency", "perplexity caching", "perplexity slow", "perplexity batch".