executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
appfolio-performance-tuningOptimize AppFolio API performance with caching and batch operations. Trigger: "appfolio performance".
Independent GitHub snapshot · Automated classification · Original documentation governs
name: appfolio-performance-tuning description: 'Optimize AppFolio API performance with caching and batch operations.
Trigger: "appfolio performance".
' allowed-tools: Read, Write, Edit, Bash(npm:), Bash(curl:), Grep version: 1.5.0 license: MIT author: Jeremy Longshore jeremy@intentsolutions.io tags:
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.
const cache = new Map<string, { data: unknown; expiry: number }>();
const MAX_CACHE_ENTRIES = 1_000;
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();
if (!cache.has(key) && cache.size >= MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value!);
cache.set(key, { data, expiry: Date.now() + TTL[ttlKey] });
return data;
}
async 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;
}
import { Agent } from 'https';
const agent = new Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 30_000 });
// Pass to axios/fetch: { httpsAgent: agent }
async 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;
}
const 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 |
For a property-dashboard regression, benchmark one synthetic portfolio page, then enable a bounded property-summary cache behind a feature flag. Compare p95 latency, request count, cache hits, and returned IDs before and after the change. Confirm that an authorized write invalidates the affected entry and that a full rate-limit response pauses new work. If the candidate yields stale, partial, unauthorized, or differently ordered results, disable the flag and reconcile before trying another optimization.
See appfolio-reference-architecture.