| 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.6.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.
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,
};
}
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.