| name | lucidchart-performance-tuning |
| description | Optimize Lucidchart API integration performance with caching, batch shape operations, and pagination strategies.
Use when diagram exports are slow, shape updates hit rate limits, or document list queries time out.
Trigger with "lucidchart performance tuning".
|
| allowed-tools | Read, Write, Edit, Grep |
| version | 1.7.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","lucidchart","diagramming"] |
| compatibility | Designed for Claude Code |
Lucidchart Performance Tuning
Overview
Lucidchart documents can contain thousands of shapes and connectors — a single enterprise diagram may hold 500+ elements across multiple pages, making bulk reads and exports the primary API bottleneck. This skill covers caching document metadata, batching shape operations, and managing Lucid's rate limits to keep integrations responsive.
Instructions
- Implement Redis caching (or in-memory Map for development) with document-appropriate TTLs
- Use cursor-based pagination for all document list operations to avoid incomplete results
- Wrap API calls with the rate limit handler, especially for bulk shape updates and exports
- Configure connection pooling with extended timeouts for export endpoints
Prerequisites
- Lucid OAuth2 client credentials with
lucidchart.document scope
- Redis instance for document/shape metadata caching
- Node.js 18+ with native fetch
- Understanding of Lucid document structure (documents, pages, shapes, lines)
Caching Strategy
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const TTL = { docList: 900, docMeta: 600, shapes: 60, exports: 300 } as const;
async function getCachedDocument(docId: string): Promise<LucidDocument> {
const key = `lucid:doc:${docId}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const doc = await lucidApi.getDocument(docId);
await redis.setex(key, TTL.docMeta, JSON.stringify(doc));
return doc;
}
async (): <[]> {
key = ;
cached = redis.(key);
(cached) .(cached);
shapes = lucidApi.(docId, pageId);
redis.(key, ., .(shapes));
shapes;
}
Batch Operations
import pLimit from "p-limit";
const limit = pLimit(4);
async function fetchAllDocuments(folderId: string): Promise<LucidDocument[]> {
const docs: LucidDocument[] = [];
let cursor: string | undefined;
do {
const page = await lucidApi.listDocuments(folderId, { cursor, limit: 100 });
docs.push(...page.documents);
cursor = page.nextCursor;
} while (cursor);
return docs;
}
async function batchUpdateShapes(
docId: string,
updates: ShapeUpdate[]
): Promise<void> {
const byPage = groupBy(updates, (u) => u.pageId);
( [pageId, pageUpdates] .(byPage)) {
chunks = (pageUpdates, );
( chunk chunks) {
.(chunk.( ( lucidApi.(docId, pageId, u))));
}
}
}
Connection Pooling
import { Agent } from "undici";
const lucidAgent = new Agent({
connect: { timeout: 10_000 },
keepAliveTimeout: 30_000,
keepAliveMaxTimeout: 60_000,
pipelining: 1,
connections: 8,
});
async function lucidFetch(path: string, init?: RequestInit): Promise<Response> {
return fetch(`https://api.lucid.co/v1${path}`, {
...init,
dispatcher: lucidAgent,
headers: { Authorization: `Bearer ${process.env.LUCID_ACCESS_TOKEN}`, ...init?.headers },
});
}
Rate Limit Management
async function withRateLimit<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
if (err.status === 429) {
const retryAfter = parseInt(err.headers?.["x-ratelimit-reset"] ?? "10", 10);
const backoff = retryAfter * 1000 * Math.pow(2, attempt);
console.warn(`Lucid rate limited. Retrying in ${backoff}ms (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, backoff));
continue;
}
throw err;
}
}
throw new Error("Lucid API: max retries exceeded");
}
Monitoring & Metrics
import { Counter, Histogram } from "prom-client";
const lucidApiLatency = new Histogram({
name: "lucidchart_api_duration_seconds",
help: "Lucid API call latency",
labelNames: ["endpoint", "status"],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
const lucidCacheHits = new Counter({
name: "lucidchart_cache_hits_total",
help: "Cache hits for Lucid document and shape data",
labelNames: ["cache_type"],
});
const lucidRateLimits = new Counter({
name: "lucidchart_rate_limits_total",
help: "Number of 429 responses from Lucid API",
});
Performance Checklist
Error Handling
| Issue | Cause | Fix |
|---|
| Timeouts on large diagram exports | PDF/PNG export of 500+ shape documents | Increase timeout to 30s, use async export with polling |
| Stale shape positions after edits | Shape cache served during collaborative editing | Lower shape TTL to 30s or invalidate on webhook |
| Pagination loops never complete | Missing cursor termination check | Always check nextCursor is defined before continuing |
| Slow document list in large workspaces | Fetching all docs without folder scoping | Filter by folder ID and use pagination with limit=100 |
| 429 during bulk diagram migration | Parallel shape creates exceed rate limit | Reduce p-limit concurrency to 2 and add 200ms delay between batches |
Output
After applying these optimizations, expect:
- Document metadata reads under 100ms (cached) vs 400ms+ (uncached)
- Shape batch updates completing 5x faster than sequential calls
- Export operations handled gracefully with async polling instead of timeout failures
Examples
const doc = await withRateLimit(() => getCachedDocument("doc-abc123"));
const shapes = await withRateLimit(() => getCachedShapes(doc.id, doc.pages[0].id));
const exportJob = await lucidApi.startExport(docId, { format: "png" });
const result = await pollUntilComplete(exportJob.id, { maxWait: 30_000 });
Resources
Next Steps
See lucidchart-reference-architecture.