Claude-code-plugins-plus appfolio-rate-limits
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/appfolio-pack/skills/appfolio-rate-limits" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-appfolio-rate-limits && rm -rf "$T"
manifest:
plugins/saas-packs/appfolio-pack/skills/appfolio-rate-limits/SKILL.mdsource content
AppFolio Rate Limits
Overview
AppFolio's Stack API enforces per-partner rate limits to protect shared property management infrastructure. High-volume operations like bulk tenant imports, rent-roll syncs, and work-order batch updates can quickly exhaust quotas. Property managers running nightly portfolio syncs across hundreds of units must throttle carefully, especially during month-end when lease renewals and payment processing spike concurrently.
Rate Limit Reference
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
| Properties list/get | 120 req | 1 minute | Per partner key |
| Tenant create/update | 30 req | 1 minute | Per partner key |
| Work orders | 60 req | 1 minute | Per partner key |
| Bulk data export | 5 req | 1 hour | Per partner key |
| Webhooks registration | 10 req | 1 minute | Per partner key |
Rate Limiter Implementation
class AppFolioRateLimiter { private tokens: number; private lastRefill: number; private readonly maxTokens: number; private readonly refillRate: number; // tokens per ms private queue: Array<{ resolve: () => void }> = []; constructor(maxPerMinute: number) { this.maxTokens = maxPerMinute; this.tokens = maxPerMinute; this.lastRefill = Date.now(); this.refillRate = maxPerMinute / 60_000; } async acquire(): Promise<void> { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return; } return new Promise(resolve => this.queue.push({ resolve })); } private refill() { const now = Date.now(); this.tokens = Math.min(this.maxTokens, this.tokens + (now - this.lastRefill) * this.refillRate); this.lastRefill = now; while (this.tokens >= 1 && this.queue.length) { this.tokens -= 1; this.queue.shift()!.resolve(); } } } const limiter = new AppFolioRateLimiter(100);
Retry Strategy
async function appfolioRetry<T>(fn: () => Promise<Response>, maxRetries = 4): Promise<T> { for (let attempt = 0; attempt <= maxRetries; attempt++) { await limiter.acquire(); const res = await fn(); if (res.ok) return res.json(); if (res.status === 429) { const retryAfter = parseInt(res.headers.get("Retry-After") || "10", 10); const delay = retryAfter * 1000 + Math.random() * 2000; await new Promise(r => setTimeout(r, delay)); continue; } if (res.status >= 500 && attempt < maxRetries) { await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000)); continue; } throw new Error(`AppFolio API ${res.status}: ${await res.text()}`); } throw new Error("Max retries exceeded"); }
Batch Processing
async function batchSyncTenants(tenants: any[], batchSize = 25) { const results: any[] = []; for (let i = 0; i < tenants.length; i += batchSize) { const batch = tenants.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(t => appfolioRetry(() => fetch(`${BASE}/api/v1/tenants`, { method: "POST", headers, body: JSON.stringify(t), }) )) ); results.push(...batchResults); if (i + batchSize < tenants.length) await new Promise(r => setTimeout(r, 2000)); } return results; }
Error Handling
| Issue | Cause | Fix |
|---|---|---|
| 429 Too Many Requests | Exceeded partner rate limit | Backoff using Retry-After header |
| 403 on bulk export | Hourly export cap reached | Queue exports with 15-min spacing |
| Timeout on property list | Large portfolio (500+ units) | Paginate with |
| 409 Conflict on tenant update | Concurrent write to same tenant | Retry with fresh ETag |
| 503 during maintenance | Scheduled nightly window (2-4 AM PT) | Skip requests, retry after window |
Resources
Next Steps
See
appfolio-performance-tuning.