Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Optimize AppFolio API performance with caching and batch operations. Trigger: "appfolio performance".
.claude/skills/jeremylongshore-appfolio-performance-tuning/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 9% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 9% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-10 | ✗→✓ | ▲ Improved | -18% | 0% |
AppFolio's property management API handles bulk tenant queries, property portfolio pagination, and work order batch processing. Large portfolios with thousands of units generate heavy read traffic on listing endpoints. Optimizing cache lifetimes for slow-changing property data, batching work order updates, and pooling HTTP connections reduces API call volume by 60-80% and cuts dashboard load times from seconds to sub-second.
typescriptconst cache = new Map<string, { data: any; expiry: number }>(); const TTL = { properties: 300_000, tenants: 120_000, units: 300_000, workOrders: 60_000 }; async function cached(key: string, ttlKey: keyof typeof TTL, fn: () => Promise<any>) { const entry = cache.get(key); if (entry && entry.expiry > Date.now()) return entry.data; const data = await fn(); cache.set(key, { data, expiry: Date.now() + TTL[ttlKey] }); return data; }
typescriptasync function batchWorkOrders(client: any, ids: string[], batchSize = 25) { const results = []; for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize); const res = await Promise.all(batch.map(id => client.http.get(`/work_orders/${id}`))); results.push(...res.map(r => r.data)); if (i + batchSize < ids.length) await new Promise(r => setTimeout(r, 200)); } return results; }
typescriptimport { Agent } from 'https'; const agent = new Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 30_000 }); // Pass to axios/fetch: { httpsAgent: agent }
typescriptasync function withRateLimit(fn: () => Promise<any>): Promise<any> { const res = await fn(); const remaining = parseInt(res.headers['x-ratelimit-remaining'] || '100'); if (remaining < 5) { const retryAfter = parseInt(res.headers['retry-after'] || '2') * 1000; await new Promise(r => setTimeout(r, retryAfter)); } return res; }
typescriptconst metrics = { apiCalls: 0, cacheHits: 0, errors: 0, totalLatency: 0 }; function track(startMs: number, hit: boolean, error?: boolean) { metrics.apiCalls++; metrics.totalLatency += Date.now() - startMs; if (hit) metrics.cacheHits++; if (error) metrics.errors++; } // Log: avg latency, cache hit rate, error rate per minute
| Issue | Cause | Fix | |-------|-------|-----| | 429 Too Many Requests | Exceeded API rate limit | Parse Retry-After header, exponential backoff | | Stale tenant data | Cache TTL too long | Reduce tenant cache to 2 min, add cache-bust on writes | | Timeout on portfolio list | Large dataset with no pagination | Add page_size=100 and cursor-based iteration | | Connection reset | Socket exhaustion | Enable keep-alive agent with maxSockets cap |
See appfolio-reference-architecture.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 17,885 | 15,467 | -14% | 1 | 1 | 0% | 4,062 | 4,793 | +18% | 0 | 0 | — |
case-02 | fail→fail | 20,225 | 14,729 | -27% | 1 | 1 | 0% | 4,361 | 4,263 | -2% | 0 | 0 | — |
case-03 | fail→fail | 17,807 | 13,777 | -23% | 1 | 1 | 0% | 3,278 | 3,619 | +10% | 0 | 0 | — |
case-16 | pass→pass | 9,142 | 1,618 | -82% | 1 | 1 | 0% | 1,584 | 1,234 | -22% | 0 | 0 | — |
case-04 | fail→pass | 10,515 | 11,298 | +7% | 1 | 1 | 0% | 1,793 | 2,706 | +51% | 0 | 0 | — |
case-05 | fail→pass | 11,856 | 5,920 | -50% | 1 | 1 | 0% | 1,854 | 2,027 | +9% | 0 | 0 | — |
case-06 | fail→pass | 11,479 | 5,976 | -48% | 1 | 1 | 0% | 1,755 | 1,912 | +9% | 0 | 0 | — |
case-07 | pass→pass | 10,520 | 7,795 | -26% | 1 | 1 | 0% | 1,792 | 2,448 | +37% | 0 | 0 | — |
case-08 | pass→pass | 11,700 | 7,977 | -32% | 1 | 1 | 0% | 1,999 | 2,387 | +19% | 0 | 0 | — |
case-09 | fail→pass | 11,785 | 8,790 | -25% | 1 | 1 | 0% | 1,980 | 2,549 | +29% | 0 | 0 | — |
case-10 | fail→pass | 10,575 | 2,589 | -76% | 1 | 1 | 0% | 1,749 | 1,434 | -18% | 0 | 0 | — |
case-11 | pass→pass | 8,626 | 2,505 | -71% | 1 | 1 | 0% | 1,432 | 1,461 | +2% | 0 | 0 | — |
case-12 | pass→pass | 10,575 | 5,699 | -46% | 1 | 1 | 0% | 1,885 | 2,018 | +7% | 0 | 0 | — |
case-13 | fail→pass | 8,631 | 3,597 | -58% | 1 | 1 | 0% | 1,557 | 1,667 | +7% | 0 | 0 | — |
case-14 | fail→pass | 18,708 | 2,582 | -86% | 1 | 1 | 0% | 976 | 1,464 | +50% | 0 | 0 | — |
case-15 | pass→pass | 13,192 | 4,763 | -64% | 1 | 1 | 0% | 2,302 | 1,863 | -19% | 0 | 0 | — |
case-17 | pass→pass | 7,965 | 3,909 | -51% | 1 | 1 | 0% | 1,289 | 1,682 | +30% | 0 | 0 | — |
case-18 | fail→pass | 14,076 | 7,110 | -49% | 1 | 1 | 0% | 2,828 | 2,418 | -14% | 0 | 0 | — |
case-19 | pass→pass | 14,131 | 9,892 | -30% | 1 | 1 | 0% | 2,330 | 2,759 | +18% | 0 | 0 | — |
case-20 | pass→pass | 6,601 | 2,614 | -60% | 1 | 1 | 0% | 1,190 | 1,465 | +23% | 0 | 0 | — |
case-21 | fail→pass | 4,907 | 2,340 | -52% | 1 | 1 | 0% | 888 | 1,474 | +66% | 0 | 0 | — |
case-22 | pass→pass | 3,399 | 3,124 | -8% | 1 | 1 | 0% | 545 | 1,510 | +177% | 0 | 0 | — |
case-23 | pass→pass | 9,156 | 4,557 | -50% | 1 | 1 | 0% | 1,544 | 1,824 | +18% | 0 | 0 | — |
case-24 | pass→pass | 3,993 | 3,878 | -3% | 1 | 1 | 0% | 690 | 1,676 | +143% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted, and 23 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +38 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.