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.md
source 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

EndpointLimitWindowScope
Properties list/get120 req1 minutePer partner key
Tenant create/update30 req1 minutePer partner key
Work orders60 req1 minutePer partner key
Bulk data export5 req1 hourPer partner key
Webhooks registration10 req1 minutePer 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

IssueCauseFix
429 Too Many RequestsExceeded partner rate limitBackoff using Retry-After header
403 on bulk exportHourly export cap reachedQueue exports with 15-min spacing
Timeout on property listLarge portfolio (500+ units)Paginate with
per_page=50
409 Conflict on tenant updateConcurrent write to same tenantRetry with fresh ETag
503 during maintenanceScheduled nightly window (2-4 AM PT)Skip requests, retry after window

Resources

Next Steps

See

appfolio-performance-tuning
.