Linear Rate Limiting
Reference: https://linear.app/developers/rate-limiting
Two budgets
Linear bills against:
- Request rate — per-IP, per-token (tier dependent: free ~1500/h, paid higher)
- Complexity points — per-token (~5000/h typical)
The complexity budget is what bites large queries. Each query has a cost based on:
- Field count
- Connection sizes (
first: 100costs more thanfirst: 10) - Nested resolves
Headers
Every response includes:
X-RateLimit-Limit— allowance per windowX-RateLimit-Remaining— remainingX-RateLimit-Reset— epoch when budget resetsX-Complexity— this query's costX-Complexity-Limit,X-Complexity-Remaining,X-Complexity-Reset
The lib/client.ts GraphQL wrapper exposes these as a callback:
client.onRateLimit((info) => {
if (info.complexityRemaining < 1000) backoff(info.resetIn);
});
Backoff strategy
When you receive a 429 (or complexity_limit GraphQL error):
- Read
X-RateLimit-Reset/X-Complexity-Reset - Sleep until that epoch + 1s jitter
- Retry once; second 429 → DLQ + alert (don't bury the user under retries)
async function withBackoff<T>(fn: () => Promise<T>, max = 3): Promise<T> {
for (let i = 0; i < max; i++) {
try { return await fn(); }
catch (e: any) {
if (e.status !== 429) throw e;
const reset = Number(e.headers["x-ratelimit-reset"]) * 1000 - Date.now();
await new Promise(r => setTimeout(r, Math.max(reset, 1000) + Math.random() * 500));
}
}
throw new Error("rate-limit retries exhausted");
}
Pacing for bulk operations
For >50 mutations, pace explicitly:
- 10 in flight max (token bucket capacity)
- 1 per 100ms refill
- Pause if
complexity_remaining < 30%
Implementation in lib/rate-limit.ts (token bucket).
Webhook back-pressure
Don't poll Linear when you can webhook instead. The bridge agents:
- Subscribe to webhooks for real-time changes
- Only poll for reconciliation (every 6h) or after webhook DLQ replay
Tips
- Prefer one big query with all fields you need over many small ones (fewer round-trips, often lower total complexity)
- Don't request
first: 250if you only need 10 - Cache slow-moving data (team list, label list) for the duration of a process
- Avoid
every:andnone:filters on large connections — they're expensive