Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build resilient Figma integrations with circuit breakers, fallbacks, and graceful degradation. Use when implementing fault tolerance, handling Figma outages gracefully, or building production-grade reliability into Figma API consumers. Trigger with phrases like "figma reliability", "figma circuit breaker", "figma fallback", "figma resilience", "figma graceful degradation".
.claude/skills/jeremylongshore-figma-reliability-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 17% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-16 | ✗→✓ | ▲ Improved | -13% | 0% |
Production reliability patterns for Figma REST API integrations. Figma is an external dependency -- your application must handle its outages, rate limits, and slow responses without cascading failures.
typescript// Prevent cascading failures when Figma is down class FigmaCircuitBreaker { private failures = 0; private lastFailure = 0; private state: 'closed' | 'open' | 'half-open' = 'closed'; constructor( private threshold = 5, // Open after 5 failures private resetTimeMs = 30_000 // Try again after 30s ) {} async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'open') { if (Date.now() - this.lastFailure > this.resetTimeMs) { this.state = 'half-open'; console.log('[figma-circuit] State: half-open (testing recovery)'); } else { throw new Error('Figma circuit breaker is OPEN -- failing fast'); } } try { const result = await fn(); if (this.state === 'half-open') { this.state = 'closed'; this.failures = 0; console.log('[figma-circuit] State: closed (recovered)'); } return result; } catch (error) { this.failures++; this.lastFailure = Date.now(); if (this.failures >= this.threshold) { this.state = 'open'; console.warn(`[figma-circuit] State: OPEN after ${this.failures} failures`); } throw error; } } getState() { return this.state; } } const figmaBreaker = new FigmaCircuitBreaker(); // Usage async function safeFigmaCall<T>(fn: () => Promise<T>): Promise<T> { return figmaBreaker.execute(fn); }
typescriptimport { readFileSync, writeFileSync, existsSync } from 'fs'; // Serve cached data when Figma is unavailable class FigmaFallbackCache { constructor(private cacheDir = '.figma-cache') {} private getPath(key: string) { return `${this.cacheDir}/${key.replace(/[^a-zA-Z0-9]/g, '_')}.json`; } save(key: string, data: any) { const { mkdirSync } = require('fs'); mkdirSync(this.cacheDir, { recursive: true }); writeFileSync(this.getPath(key), JSON.stringify({ data, cachedAt: new Date().toISOString(), })); } load(key: string): { data: any; cachedAt: string } | null { const path = this.getPath(key); if (!existsSync(path)) return null; return JSON.parse(readFileSync(path, 'utf-8')); } } const fallbackCache = new FigmaFallbackCache(); async function fetchWithFallback<T>( cacheKey: string, fetcher: () => Promise<T> ): Promise<{ data: T; fromCache: boolean; cachedAt?: string }> { try { const data = await safeFigmaCall(fetcher); // Update cache with fresh data fallbackCache.save(cacheKey, data); return { data, fromCache: false }; } catch (error) { console.warn(`Figma unavailable, loading cached ${cacheKey}`); const cached = fallbackCache.load(cacheKey); if (cached) { return { data: cached.data as T, fromCache: true, cachedAt: cached.cachedAt }; } throw new Error(`Figma unavailable and no cached data for ${cacheKey}`); } }
typescriptasync function figmaRetry<T>( fn: () => Promise<Response>, maxRetries = 3 ): Promise<T> { for (let attempt = 0; attempt <= maxRetries; attempt++) { const res = await fn(); if (res.ok) return res.json(); if (res.status === 429) { const retryAfter = parseInt(res.headers.get('Retry-After') || '60'); if (attempt < maxRetries) { console.warn(`429 -- waiting ${retryAfter}s (attempt ${attempt + 1}/${maxRetries})`); await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } } if (res.status >= 500 && attempt < maxRetries) { const delay = Math.min(1000 * Math.pow(2, attempt), 30_000); const jitter = Math.random() * 1000; await new Promise(r => setTimeout(r, delay + jitter)); continue; } throw new FigmaApiError(res.status, await res.text()); } throw new Error('Max retries exceeded'); }
typescript// Prevent requests from hanging indefinitely async function figmaFetchWithTimeout( path: string, token: string, timeoutMs = 15_000 ): Promise<Response> { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(`https://api.figma.com${path}`, { headers: { 'X-Figma-Token': token }, signal: controller.signal, }); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Figma request timed out after ${timeoutMs}ms: ${path}`); } throw error; } finally { clearTimeout(timeout); } }
typescript// Only make non-critical Figma calls when the API is healthy class FigmaHealthTracker { private healthy = true; private lastCheck = 0; private checkIntervalMs = 30_000; async isHealthy(token: string): Promise<boolean> { if (Date.now() - this.lastCheck < this.checkIntervalMs) { return this.healthy; } try { const res = await figmaFetchWithTimeout('/v1/me', token, 5000); this.healthy = res.ok; } catch { this.healthy = false; } this.lastCheck = Date.now(); return this.healthy; } } const healthTracker = new FigmaHealthTracker(); async function conditionalFigmaCall<T>( token: string, critical: boolean, fn: () => Promise<T>, fallback: () => Promise<T> ): Promise<T> { const healthy = await healthTracker.isHealthy(token); if (!healthy && !critical) { console.log('Figma unhealthy, using fallback for non-critical call'); return fallback(); } return fetchWithFallback('default', fn).then(r => r.data); }
Retry-After header| Issue | Cause | Solution | |-------|-------|----------| | Circuit stays open | Threshold too low | Increase threshold or decrease reset time | | Stale fallback data | Cache not refreshed | Refresh cache on successful calls | | Retry loops | Not respecting Retry-After | Always use the header value | | Timeout too short | Large file responses | Increase timeout for /v1/files calls |
Watch the circuit breaker (Step 1) do its job during a Figma incident:
text12:04:11 figma request failed (503) — failure 1/5 12:04:13 figma request failed (503) — failure 5/5 → circuit OPEN for 30s 12:04:14 request short-circuited; serving cached tokens (age 8m) via fallback 12:04:44 circuit HALF-OPEN — probe /v1/me → 200 → circuit CLOSED
Confirm retry honors Retry-After instead of hammering (Step 3):
textGET /v1/files/abc → 429 (Retry-After: 32) sleeping 32s (server-directed, overrides backoff schedule) GET /v1/files/abc → 200 (attempt 2)
The cached-fallback contract (what staleness is acceptable per consumer) is in references/cached-fallback.md; composition of all five patterns: references/health-aware-request-routing.md.
For policy enforcement, see figma-policy-guardrails.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 30,888 | 28,712 | -7% | 1 | 1 | 0% | 6,661 | 7,100 | +7% | 0 | 0 | — |
case-02 | fail→fail | 21,004 | 26,162 | +25% | 1 | 1 | 0% | 4,295 | 6,923 | +61% | 0 | 0 | — |
case-03 | fail→pass | 36,857 | 23,308 | -37% | 1 | 1 | 0% | 6,277 | 7,360 | +17% | 0 | 0 | — |
case-04 | pass→pass | 15,231 | 4,545 | -70% | 1 | 1 | 0% | 3,157 | 3,248 | +3% | 0 | 0 | — |
case-05 | pass→pass | 18,046 | 10,997 | -39% | 1 | 1 | 0% | 3,425 | 4,074 | +19% | 0 | 0 | — |
case-06 | pass→pass | 21,353 | 11,558 | -46% | 1 | 1 | 0% | 3,341 | 4,499 | +35% | 0 | 0 | — |
case-07 | fail→fail | 19,088 | 14,628 | -23% | 1 | 1 | 0% | 3,066 | 4,758 | +55% | 0 | 0 | — |
case-08 | fail→fail | 16,881 | 13,762 | -18% | 1 | 1 | 0% | 2,660 | 5,178 | +95% | 0 | 0 | — |
case-09 | fail→pass | 16,817 | 15,561 | -7% | 1 | 1 | 0% | 3,261 | 5,441 | +67% | 0 | 0 | — |
case-10 | pass→pass | 11,930 | 7,167 | -40% | 1 | 1 | 0% | 2,527 | 3,897 | +54% | 0 | 0 | — |
case-11 | fail→pass | 15,706 | 5,247 | -67% | 1 | 1 | 0% | 2,829 | 3,376 | +19% | 0 | 0 | — |
case-12 | pass→pass | 13,555 | 8,312 | -39% | 1 | 1 | 0% | 2,698 | 3,740 | +39% | 0 | 0 | — |
case-13 | fail→pass | 18,663 | 15,674 | -16% | 1 | 1 | 0% | 3,424 | 5,394 | +58% | 0 | 0 | — |
case-14 | pass→pass | 16,786 | 12,277 | -27% | 1 | 1 | 0% | 2,868 | 4,556 | +59% | 0 | 0 | — |
case-15 | pass→pass | 11,761 | 3,902 | -67% | 1 | 1 | 0% | 2,110 | 3,063 | +45% | 0 | 0 | — |
case-16 | fail→pass | 18,769 | 6,614 | -65% | 1 | 1 | 0% | 4,119 | 3,594 | -13% | 0 | 0 | — |
case-17 | pass→pass | 13,675 | 14,611 | +7% | 1 | 1 | 0% | 2,527 | 4,576 | +81% | 0 | 0 | — |
case-18 | pass→pass | 5,224 | 3,419 | -35% | 1 | 1 | 0% | 871 | 2,972 | +241% | 0 | 0 | — |
case-19 | pass→pass | 10,717 | 3,987 | -63% | 1 | 1 | 0% | 1,682 | 2,945 | +75% | 0 | 0 | — |
case-20 | fail→pass | 9,043 | 3,827 | -58% | 1 | 1 | 0% | 1,438 | 2,933 | +104% | 0 | 0 | — |
case-21 | pass→pass | 15,316 | 16,498 | +8% | 1 | 1 | 0% | 2,976 | 5,802 | +95% | 0 | 0 | — |
case-22 | fail→pass | 12,649 | 23,837 | +88% | 1 | 1 | 0% | 2,440 | 6,726 | +176% | 0 | 0 | — |
case-23 | pass→pass | 12,968 | 12,905 | -0% | 1 | 1 | 0% | 2,658 | 4,337 | +63% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +30 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.