'Optimize Miro REST API v2 performance with caching, cursor pagination,
Scanned 9/2/2026
Install to Claude Code
npx -y skills add jeremylongshore/tons-of-skills-marketplace --skill miro-performance-tuning --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Miro Performance Tuning?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-miro-performance-tuning-tons-of-skills-marketplace)More formats (shields.io, HTML) on the badges page.
---
name: miro-performance-tuning
description: 'Optimize Miro REST API v2 performance with caching, cursor pagination,
request batching, and connection pooling for high-throughput integrations.
Trigger with phrases like "miro performance", "optimize miro",
"miro latency", "miro caching", "miro slow", "miro batch".
'
allowed-tools: Read, Write, Edit
version: 1.7.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- miro
- performance
- caching
compatibility: Designed for Claude Code
---
# Miro Performance Tuning
## Overview
Optimize Miro REST API v2 throughput and latency. Key levers: minimize API calls with cursor pagination, cache board/item data, batch writes with controlled concurrency, and use connection pooling.
## 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.
## Latency Benchmarks
Typical latencies for `api.miro.com` (US region):
| Operation | Endpoint | P50 | P95 | Credits |
|-----------|----------|-----|-----|---------|
| Get board | `GET /v2/boards/{id}` | 80ms | 200ms | Level 1 |
| List items (50) | `GET /v2/boards/{id}/items?limit=50` | 120ms | 350ms | Level 1 |
| Create sticky note | `POST /v2/boards/{id}/sticky_notes` | 150ms | 400ms | Level 2 |
| Create connector | `POST /v2/boards/{id}/connectors` | 160ms | 420ms | Level 2 |
| Update item | `PATCH /v2/boards/{id}/items/{id}` | 130ms | 350ms | Level 2 |
| Delete item | `DELETE /v2/boards/{id}/items/{id}` | 100ms | 280ms | Level 2 |
## Cursor Pagination (Eliminate Over-Fetching)
Miro v2 uses cursor-based pagination. Fetch only what you need.
```typescript
// Efficient paginated iterator
async function* paginateItems(
boardId: string,
options: { type?: string; limit?: number } = {}
): AsyncGenerator<MiroBoardItem> {
const limit = options.limit ?? 50; // Max 50 per page
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: String(limit) });
if (options.type) params.set('type', options.type);
if (cursor) params.set('cursor', cursor);
const response = await fetch(
`https://api.miro.com/v2/boards/${boardId}/items?${params}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const page = await response.json();
for (const item of page.data) {
yield item;
}
cursor = page.cursor; // undefined when no more pages
} while (cursor);
}
// Usage: process items without loading entire board into memory
for await (const item of paginateItems(boardId, { type: 'sticky_note' })) {
await processItem(item);
}
```
## Caching Strategy
### In-Memory Cache (Single Instance)
```typescript
import { LRUCache } from 'lru-cache';
const boardCache = new LRUCache<string, unknown>({
max: 500, // Max 500 cached entries
ttl: 60_000, // 1 minute TTL
updateAgeOnGet: true, // Extend TTL on access
updateAgeOnHas: false,
});
async function getCachedBoard(boardId: string): Promise<MiroBoard> {
const cacheKey = `board:${boardId}`;
const cached = boardCache.get(cacheKey);
if (cached) return cached as MiroBoard;
const board = await miroFetch(`/v2/boards/${boardId}`);
boardCache.set(cacheKey, board);
return board;
}
// Invalidate on webhook events
function onBoardEvent(event: MiroBoardEvent) {
boardCache.delete(`board:${event.boardId}`);
boardCache.delete(`items:${event.boardId}`);
}
```
### Redis Cache (Distributed)
```typescript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedItems(boardId: string): Promise<MiroBoardItem[]> {
const key = `miro:items:${boardId}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Fetch all items
const items: MiroBoardItem[] = [];
for await (const item of paginateItems(boardId)) {
items.push(item);
}
// Cache for 2 minutes (boards with active editing should use shorter TTL)
await redis.setex(key, 120, JSON.stringify(items));
return items;
}
// Webhook-driven cache invalidation
async function invalidateOnEvent(event: MiroBoardEvent) {
const keys = [
`miro:items:${event.boardId}`,
`miro:board:${event.boardId}`,
];
await redis.del(...keys);
}
```
## Controlled Concurrency for Bulk Operations
```typescript
import PQueue from 'p-queue';
// Create items in parallel with controlled concurrency
async function bulkCreateStickyNotes(
boardId: string,
notes: Array<{ content: string; color: string; x: number; y: number }>
): Promise<string[]> {
const queue = new PQueue({
concurrency: 5, // Max 5 parallel requests
interval: 1000, // Per-second window
intervalCap: 10, // Max 10 requests per second
});
const ids: string[] = [];
for (const note of notes) {
queue.add(async () => {
const result = await miroFetch(`/v2/boards/${boardId}/sticky_notes`, 'POST', {
data: { content: note.content, shape: 'square' },
style: { fillColor: note.color },
position: { x: note.x, y: note.y },
});
ids.push(result.id);
});
}
await queue.onIdle(); // Wait for all to complete
return ids;
}
// Example: Create a grid of 50 sticky notes efficiently
const notes = Array.from({ length: 50 }, (_, i) => ({
content: `Note ${i + 1}`,
color: 'light_yellow',
x: (i % 10) * 250,
y: Math.floor(i / 10) * 250,
}));
const createdIds = await bulkCreateStickyNotes(boardId, notes);
console.log(`Created ${createdIds.length} sticky notes`);
```
## Connection Pooling
```typescript
import { Agent } from 'https';
// Reuse TCP connections across requests
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 10, // Max 10 concurrent connections to api.miro.com
maxFreeSockets: 5, // Keep 5 idle connections in pool
timeout: 30000, // Socket timeout
});
async function miroFetchPooled(path: string, options: RequestInit = {}) {
// Note: Node.js native fetch doesn't support custom agents directly.
// Use undici or node-fetch for connection pooling.
const { default: fetch } = await import('node-fetch');
return fetch(`https://api.miro.com${path}`, {
...options,
agent: httpsAgent,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
}
```
## Performance Monitoring
```typescript
async function instrumentedMiroFetch<T>(
operation: string,
path: string,
method = 'GET',
body?: unknown,
): Promise<{ data: T; metrics: RequestMetrics }> {
const start = performance.now();
const response = await fetch(`https://api.miro.com${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
...(body ? { body: JSON.stringify(body) } : {}),
});
const duration = performance.now() - start;
const metrics: RequestMetrics = {
operation,
path,
method,
status: response.status,
durationMs: Math.round(duration),
rateLimitRemaining: parseInt(response.headers.get('X-RateLimit-Remaining') ?? '0', 10),
rateLimitReset: response.headers.get('X-RateLimit-Reset') ?? '',
};
// Log for dashboarding
console.log('[MIRO_PERF]', JSON.stringify(metrics));
const data = response.status !== 204 ? await response.json() : null;
return { data: data as T, metrics };
}
```
## Optimization Checklist
| Technique | Impact | Effort | When to Use |
|-----------|--------|--------|-------------|
| Type-filtered queries | High | Low | Always — `?type=sticky_note` reduces payload |
| LRU cache | High | Low | Read-heavy workloads |
| Redis cache + webhook invalidation | Very High | Medium | Multi-instance deployments |
| Controlled concurrency | High | Low | Bulk create/update operations |
| Connection pooling | Medium | Low | High request volume |
| Cursor pagination | High | Low | Always — avoid loading full board |
## 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
| Issue | Cause | Solution |
|-------|-------|----------|
| Cache stale data | Long TTL + frequent edits | Shorten TTL or use webhook invalidation |
| Concurrency errors | Too many parallel requests | Reduce `concurrency` in PQueue |
| Connection pool exhausted | Max sockets too low | Increase `maxSockets` |
| Pagination cursor expired | Long gap between pages | Re-start pagination from beginning |
## Resources
- [Get Items on Board](https://developers.miro.com/reference/get-items)
- [Rate Limiting](https://developers.miro.com/reference/rate-limiting)
- [LRU Cache](https://github.com/isaacs/node-lru-cache)
- [p-queue](https://github.com/sindresorhus/p-queue)
## Next Steps
For cost optimization, see `miro-cost-tuning`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!