agentsclimarketplace

Appfolio rate limits

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/appfolio-pack/skills/appfolio-rate-limits

'Handle AppFolio API rate limits with throttling and backoff.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill appfolio-rate-limits

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

4.2 KB, 981 tokens by cl100k_base, as published. Nobody here has run it

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.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.