| name | miro-rate-limits |
| description | Implement Miro REST API v2 rate limiting with the credit-based system,
exponential backoff, and request queuing.
Trigger with phrases like "miro rate limit", "miro throttling",
"miro 429", "miro retry", "miro backoff", "miro credits".
|
| allowed-tools | Read, Write, Edit |
| version | 1.7.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","miro","rate-limits","performance"] |
| compatibility | Designed for Claude Code |
Miro Rate Limits
Overview
Miro measures API usage in credits, not raw request counts. Each endpoint consumes a different number of credits based on complexity. The global limit is 100,000 credits per minute per app.
Prerequisites
Before applying this guide, confirm you have a Miro app or workspace appropriate to the task, a dedicated non-production board where changes can be tested safely, and only the OAuth scopes or administrative access the procedure requires.
Credit System
Rate Limit Levels
Each Miro REST API endpoint is assigned a rate limit level that determines its credit cost:
| Level | Credits per Call | Example Endpoints |
|---|
| Level 1 | Lower cost | GET single board, GET single item |
| Level 2 | Medium cost | POST create sticky note, POST create shape, POST create connector |
| Level 3 | Higher cost | Batch operations, complex queries |
| Level 4 | Highest cost | Export, bulk data operations |
The exact credit cost per level is subject to change. Monitor via response headers.
Rate Limit Response Headers
Every Miro API response includes these headers:
| Header | Description | Example |
|---|
X-RateLimit-Limit | Total credits allocated per minute | 100000 |
X-RateLimit-Remaining | Credits remaining in current window | 99850 |
X-RateLimit-Reset | Unix timestamp when window resets | 1700000060 |
When rate limited, the response also includes:
| Header | Description | Example |
|---|
Retry-After | Seconds to wait before retrying | 30 |
Exponential Backoff with Jitter
interface BackoffConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
jitterMs: number;
}
const DEFAULT_BACKOFF: BackoffConfig = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 32000,
jitterMs: 500,
};
async function withBackoff<T>(
operation: () => Promise<Response>,
config = DEFAULT_BACKOFF
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
const response = await operation();
if (response.ok) {
return response.json();
}
if (response.status !== 429 && response.status < 500) {
const error = await response.json().catch(() => ({}));
();
}
(attempt === config.) {
();
}
retryAfter = response..();
: ;
(retryAfter) {
delay = (retryAfter, ) * ;
} {
exponential = config. * .(, attempt);
jitter = .() * config.;
delay = .(exponential + jitter, config.);
}
.(
);
( (r, delay));
}
();
}
board = withBackoff<>(
(, {
: { : },
})
);
Rate Limit Monitor
class MiroRateLimitMonitor {
private remaining = 100000;
private resetAt = 0;
private windowCreditsUsed = 0;
updateFromResponse(response: Response): void {
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (remaining) this.remaining = parseInt(remaining, 10);
if (reset) this.resetAt = parseInt(reset, 10) * 1000;
if (limit) {
this.windowCreditsUsed = parseInt(limit, 10) - this.remaining;
}
}
shouldThrottle(): boolean {
return this.remaining < && .() < .;
}
(): {
(!.()) ;
.(, . - .());
}
(): { : ; : ; : } {
{
: .,
: .((. / ) * ),
: .(, . - .()),
};
}
}
Request Queue (p-queue)
For high-throughput integrations, queue requests to stay within limits.
import PQueue from 'p-queue';
const monitor = new MiroRateLimitMonitor();
const miroQueue = new PQueue({
concurrency: 5,
interval: 1000,
intervalCap: 10,
timeout: 30000,
});
async function queuedMiroFetch(path: string, options?: RequestInit) {
const waitMs = monitor.getWaitMs();
if (waitMs > 0) {
console.warn(`[Miro] Throttling: waiting ${waitMs}ms for rate limit reset`);
await new Promise(r => setTimeout(r, waitMs));
}
return miroQueue.add(async () => {
const response = await fetch(, {
...options,
: {
: ,
: ,
...options?.,
},
});
monitor.(response);
(!response.) {
(response. === ) {
retryAfter = (response..() ?? , );
( (r, retryAfter * ));
(path, options);
}
();
}
response.();
});
}
Batch Operations to Reduce Credit Usage
for (const id of itemIds) {
const item = await miroFetch(`/v2/boards/${boardId}/items/${id}`);
}
const allItems = await miroFetch(`/v2/boards/${boardId}/items?limit=50`);
const wantedItems = allItems.data.filter(item => itemIds.includes(item.id));
const stickyNotes = await miroFetch(`/v2/boards/${boardId}/items?type=sticky_note&limit=50`);
Cost Estimation
function estimateCreditsPerMinute(
requestsPerMinute: number,
avgLevel: 1 | 2 | 3 | 4
): { credits: number; percentOfLimit: number; safe: boolean } {
const creditCost = { 1: 5, 2: 10, 3: 20, 4: 50 };
const credits = requestsPerMinute * creditCost[avgLevel];
return {
credits,
percentOfLimit: Math.round((credits / 100000) * 100),
safe: credits < 80000,
};
}
Instructions
Use the ordered procedures and code samples in this guide as a sequence: begin with the prerequisites, apply the configuration or operational step for the target environment, then perform the documented validation or cleanup before proceeding. Keep credentials in the documented secret store; never hard-code them in source.
Output
Following this guide produces the Miro integration outcome for its topic—configuration, validation evidence, operational recovery, or a documented migration result. Record command output and relevant identifiers so a failed step is traceable.
Examples
Start with the smallest applicable command or code example in the relevant section, using a dedicated test board and non-production credentials. Confirm the expected response or validation result before applying the pattern to production.
Error Handling
| Scenario | Detection | Action |
|---|
| Approaching limit | X-RateLimit-Remaining < 5000 | Reduce request frequency |
| Rate limited | HTTP 429 | Backoff using Retry-After header |
| Sustained 429s | Multiple consecutive 429s | Pause all requests, wait for reset |
| Credit spike | Monitor shows >80% usage | Audit for unnecessary requests |
Resources
Next Steps
For security configuration, see miro-security-basics.