From canva-pack
Manages Canva Connect API rate limits using exponential backoff with jitter and PQueue queuing for endpoints like designs, exports, assets. Prevents 429 errors in integrations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/canva-pack:canva-rate-limitsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
The Canva Connect API enforces per-user, per-endpoint rate limits. Each endpoint has different thresholds. A 429 response means you must wait before retrying.
The Canva Connect API enforces per-user, per-endpoint rate limits. Each endpoint has different thresholds. A 429 response means you must wait before retrying.
| Endpoint | Method | Limit |
|---|---|---|
/v1/users/me | GET | 10 req/min |
/v1/users/me/profile | GET | 10 req/min |
/v1/designs | GET | 100 req/min |
/v1/designs | POST | 20 req/min |
/v1/designs/{id} | GET | 100 req/min |
/v1/exports | POST | 75 req/5min, 500/24hr per user |
/v1/exports (integration) | POST | 750 req/5min, 5000/24hr |
/v1/exports (per document) | POST | 75 req/5min |
/v1/asset-uploads | POST | 30 req/min |
/v1/autofills | POST | 60 req/min |
/v1/folders | POST | 20 req/min |
/v1/brand-templates | GET | 100 req/min |
All limits are per user of your integration unless noted otherwise.
async function canvaRequestWithBackoff<T>(
fn: () => Promise<T>,
config = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 }
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (attempt === config.maxRetries) throw error;
// Only retry on 429 or 5xx
const status = error.status || error.response?.status;
if (status !== 429 && (status < 500 || status >= 600)) throw error;
// Honor Retry-After header if present
const retryAfter = error.headers?.get?.('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(
config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
config.maxDelayMs
);
console.warn(`Rate limited (attempt ${attempt + 1}/${config.maxRetries}). Waiting ${(delay / 1000).toFixed(1)}s`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
import PQueue from 'p-queue';
// Match per-user endpoint limits
const canvaQueues = {
designs: new PQueue({ concurrency: 1, interval: 3000, intervalCap: 1 }), // ~20/min
exports: new PQueue({ concurrency: 1, interval: 4000, intervalCap: 1 }), // ~15/min (conservative)
assets: new PQueue({ concurrency: 1, interval: 2000, intervalCap: 1 }), // ~30/min
reads: new PQueue({ concurrency: 3, interval: 1000, intervalCap: 3 }), // ~100/min (shared reads)
};
// Usage — automatically queued to stay under limits
const design = await canvaQueues.designs.add(() =>
client.createDesign({ design_type: { type: 'custom', width: 1080, height: 1080 }, title: 'Queued' })
);
// Batch export with rate control
const designIds = ['DAV1', 'DAV2', 'DAV3', 'DAV4', 'DAV5'];
const exports = await Promise.all(
designIds.map(id =>
canvaQueues.exports.add(() =>
client.createExport({ design_id: id, format: { type: 'pdf' } })
)
)
);
class CanvaRateLimitTracker {
private windows: Map<string, { count: number; resetAt: number }> = new Map();
track(endpoint: string, response: Response): void {
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (remaining !== null) {
this.windows.set(endpoint, {
count: parseInt(remaining),
resetAt: reset ? parseInt(reset) * 1000 : Date.now() + 60000,
});
}
}
shouldThrottle(endpoint: string): boolean {
const window = this.windows.get(endpoint);
if (!window) return false;
return window.count < 3 && Date.now() < window.resetAt;
}
getWaitMs(endpoint: string): number {
const window = this.windows.get(endpoint);
if (!window) return 0;
return Math.max(0, window.resetAt - Date.now());
}
report(): Record<string, { remaining: number; resetsIn: string }> {
const report: Record<string, any> = {};
for (const [ep, w] of this.windows) {
report[ep] = {
remaining: w.count,
resetsIn: `${Math.max(0, (w.resetAt - Date.now()) / 1000).toFixed(0)}s`,
};
}
return report;
}
}
// Wrap the client to throttle before hitting limits
async function throttledCanvaRequest<T>(
tracker: CanvaRateLimitTracker,
endpoint: string,
fn: () => Promise<T>
): Promise<T> {
if (tracker.shouldThrottle(endpoint)) {
const waitMs = tracker.getWaitMs(endpoint);
console.log(`Proactively waiting ${waitMs}ms for ${endpoint}`);
await new Promise(r => setTimeout(r, waitMs));
}
return fn();
}
| Scenario | Detection | Action |
|---|---|---|
| Single 429 | HTTP status | Wait Retry-After seconds, retry |
| Sustained 429s | Multiple retries fail | Reduce request rate, increase backoff |
| Export quota hit | 500/24hr per user | Queue exports, spread across hours |
| Integration quota | 5000/24hr exports | Distribute across users |
For security configuration, see canva-security-basics.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin canva-packImplements circuit breakers, graceful degradation, retries for resilient Canva Connect API integrations handling timeouts, rate limits, and async jobs.
Manages Figma REST API rate limits with exponential backoff, Retry-After parsing, jittered retries, and queuing. Handles 429 errors for reliable throughput.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.