AppFolio Cost Tuning
Overview
AppFolio Stack API pricing is partner-agreement based, with costs scaling by API call volume per managed property. Property management portfolios generate high-frequency reads for tenant lookups, lease status checks, and maintenance requests. Each redundant API call erodes margin on per-unit revenue. Optimizing call patterns directly impacts operational profitability, especially for portfolios managing hundreds or thousands of units where even small per-call costs compound rapidly.
Prerequisites
- The current partner agreement’s actual billing, endpoint quota, export, and
event-delivery terms; treat published examples as planning inputs, not price
commitments.
- A data classification and retention policy that prevents tenant, lease, and
financial payloads from being retained in generic process memory or caches.
- Per-endpoint call budgets, cache owners, and a reconciliation path for stale
reads that could affect accounting, lease, or maintenance decisions.
Instructions
- Measure the existing call rate, cache hit rate, payload sizes, and provider
charges by endpoint before changing a TTL or polling interval.
- Cache only the minimized, non-sensitive fields required by the caller, with
a bounded size and an endpoint-specific freshness policy.
- Use incremental reads or provider-supported events only after verifying the
partner capability and loss/replay semantics; otherwise use bounded polling.
- Stop or degrade non-critical work at the approved budget threshold and send
stale or incomplete financial/lease data to an operator rather than guessing.
Cost Breakdown
| Component |
Cost Driver |
Optimization |
| Property/unit reads |
Per-call pricing on tenant and unit endpoints |
Cache with 10-15 min TTL; property data changes infrequently |
| Lease operations |
Bulk lease queries across entire portfolio |
Fetch all leases once, filter locally instead of per-unit calls |
| Maintenance requests |
Polling for new work orders |
Use verified provider events, or bounded incremental polling |
| Reporting exports |
Large payload downloads for financial reports |
Schedule off-peak, cache results for 24h |
| Vendor/owner lookups |
Repeated lookups for the same contacts |
Build a local lookup table, refresh daily |
API Call Reduction
class AppFolioCache {
private cache = new Map<string, { data: unknown; expiry: number }>();
private readonly maxEntries = 1_000;
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry || Date.now() > entry.expiry) return null;
return entry.data;
}
set(key: string, data: unknown, ttlMs = 600_000): void {
if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
this.cache.delete(this.cache.keys().next().value!);
}
this.cache.set(key, { data, expiry: Date.now() + ttlMs });
}
async fetchWithCache(endpoint: string, ttlMs?: number): Promise<any> {
const cached = this.get(endpoint);
if (cached) return cached;
const response = await fetch(endpoint);
const data = await response.json();
this.set(endpoint, data, ttlMs);
return data;
}
}
Usage Monitoring
class AppFolioUsageMonitor {
private calls: Array<{ endpoint: string; timestamp: number }> = [];
private budgetLimit = 10_000; // daily call budget
record(endpoint: string): void {
this.calls.push({ endpoint, timestamp: Date.now() });
const todayCalls = this.getTodayCount();
if (todayCalls > this.budgetLimit * 0.8) {
console.warn(`AppFolio API budget 80% consumed: ${todayCalls}/${this.budgetLimit}`);
}
}
getTodayCount(): number {
const startOfDay = new Date().setHours(0, 0, 0, 0);
return this.calls.filter(c => c.timestamp > startOfDay).length;
}
}
Cost Optimization Checklist
Error Handling
| Issue |
Cause |
Fix |
| 429 Too Many Requests |
Exceeded rate limit |
Implement exponential backoff with jitter |
| Stale cache serving old data |
TTL too long for volatile data |
Reduce TTL for maintenance/lease endpoints to 2-5 min |
| Budget alerts firing daily |
Polling loop running on short interval |
Switch to webhook-driven architecture |
| Duplicate API calls |
Multiple services fetching same data |
Centralize through shared cache layer |
| Large payload timeouts |
Fetching full portfolio in single call |
Paginate requests, process in batches of 100 |
Output
- A measured per-endpoint call and cache budget with an accountable owner
- A bounded, minimized cache policy and a clear stale-data decision boundary
- A documented decision to pause, defer, or reconcile work when the budget,
provider capability, or data freshness requirement cannot be met
Examples
For a nightly property-status sync, capture a baseline call count and response
size, cache only the property ID and permitted occupancy summary, and cap the
cache at the approved entry count. Enable an incremental cursor only after a
staging replay proves no records are lost; otherwise retain low-frequency,
rate-limited polling. When the budget alert fires, stop non-critical refreshes
and surface the age of the last verified result. Do not use cached or partial
data to make a lease, payment, or safety decision without operator review.
Resources
Next Steps
See appfolio-performance-tuning.
1---2name: appfolio-cost-tuning3description: Optimize AppFolio API costs through efficient usage patterns. Trigger: "appfolio cost".4license: MIT5---6# AppFolio Cost Tuning
7
8## Overview
9
10AppFolio Stack API pricing is partner-agreement based, with costs scaling by API call volume per managed property. Property management portfolios generate high-frequency reads for tenant lookups, lease status checks, and maintenance requests. Each redundant API call erodes margin on per-unit revenue. Optimizing call patterns directly impacts operational profitability, especially for portfolios managing hundreds or thousands of units where even small per-call costs compound rapidly.
11
12## Prerequisites
13
14- The current partner agreement’s actual billing, endpoint quota, export, and
15 event-delivery terms; treat published examples as planning inputs, not price
16 commitments.
17- A data classification and retention policy that prevents tenant, lease, and
18 financial payloads from being retained in generic process memory or caches.
19- Per-endpoint call budgets, cache owners, and a reconciliation path for stale
20 reads that could affect accounting, lease, or maintenance decisions.
21
22## Instructions
23
241. Measure the existing call rate, cache hit rate, payload sizes, and provider
25 charges by endpoint before changing a TTL or polling interval.
262. Cache only the minimized, non-sensitive fields required by the caller, with
27 a bounded size and an endpoint-specific freshness policy.
283. Use incremental reads or provider-supported events only after verifying the
29 partner capability and loss/replay semantics; otherwise use bounded polling.
304. Stop or degrade non-critical work at the approved budget threshold and send
31 stale or incomplete financial/lease data to an operator rather than guessing.
32
33## Cost Breakdown
34
35| Component | Cost Driver | Optimization |
36|-----------|------------|--------------|
37| Property/unit reads | Per-call pricing on tenant and unit endpoints | Cache with 10-15 min TTL; property data changes infrequently |
38| Lease operations | Bulk lease queries across entire portfolio | Fetch all leases once, filter locally instead of per-unit calls |
39| Maintenance requests | Polling for new work orders | Use verified provider events, or bounded incremental polling |
40| Reporting exports | Large payload downloads for financial reports | Schedule off-peak, cache results for 24h |
41| Vendor/owner lookups | Repeated lookups for the same contacts | Build a local lookup table, refresh daily |
42
43## API Call Reduction
44
45```typescript
46class AppFolioCache {
47 private cache = new Map<string, { data: unknown; expiry: number }>();
48 private readonly maxEntries = 1_000;
49
50 get(key: string): any | null {
51 const entry = this.cache.get(key);
52 if (!entry || Date.now() > entry.expiry) return null;
53 return entry.data;
54 }
55
56 set(key: string, data: unknown, ttlMs = 600_000): void {
57 if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
58 this.cache.delete(this.cache.keys().next().value!);
59 }
60 this.cache.set(key, { data, expiry: Date.now() + ttlMs });
61 }
62
63 async fetchWithCache(endpoint: string, ttlMs?: number): Promise<any> {
64 const cached = this.get(endpoint);
65 if (cached) return cached;
66 const response = await fetch(endpoint);
67 const data = await response.json();
68 this.set(endpoint, data, ttlMs);
69 return data;
70 }
71}
72```
73
74## Usage Monitoring
75
76```typescript
77class AppFolioUsageMonitor {
78 private calls: Array<{ endpoint: string; timestamp: number }> = [];
79 private budgetLimit = 10_000; // daily call budget
80
81 record(endpoint: string): void {
82 this.calls.push({ endpoint, timestamp: Date.now() });
83 const todayCalls = this.getTodayCount();
84 if (todayCalls > this.budgetLimit * 0.8) {
85 console.warn(`AppFolio API budget 80% consumed: ${todayCalls}/${this.budgetLimit}`);
86 }
87 }
88
89 getTodayCount(): number {
90 const startOfDay = new Date().setHours(0, 0, 0, 0);
91 return this.calls.filter(c => c.timestamp > startOfDay).length;
92 }
93}
94```
95
96## Cost Optimization Checklist
97
98- [ ] Cache property and unit data with 10-15 min TTL
99- [ ] Replace polling loops with verified event delivery or bounded incremental polling
100- [ ] Batch lease queries — fetch all, filter locally
101- [ ] Use incremental sync with `modified_since` parameter
102- [ ] Schedule report exports during off-peak hours
103- [ ] Build local lookup tables for vendors and owners
104- [ ] Set daily API call budget alerts at 80% threshold
105- [ ] Audit unused integrations consuming API quota
106
107## Error Handling
108
109| Issue | Cause | Fix |
110|-------|-------|-----|
111| 429 Too Many Requests | Exceeded rate limit | Implement exponential backoff with jitter |
112| Stale cache serving old data | TTL too long for volatile data | Reduce TTL for maintenance/lease endpoints to 2-5 min |
113| Budget alerts firing daily | Polling loop running on short interval | Switch to webhook-driven architecture |
114| Duplicate API calls | Multiple services fetching same data | Centralize through shared cache layer |
115| Large payload timeouts | Fetching full portfolio in single call | Paginate requests, process in batches of 100 |
116
117## Output
118
119- A measured per-endpoint call and cache budget with an accountable owner
120- A bounded, minimized cache policy and a clear stale-data decision boundary
121- A documented decision to pause, defer, or reconcile work when the budget,
122 provider capability, or data freshness requirement cannot be met
123
124## Examples
125
126For a nightly property-status sync, capture a baseline call count and response
127size, cache only the property ID and permitted occupancy summary, and cap the
128cache at the approved entry count. Enable an incremental cursor only after a
129staging replay proves no records are lost; otherwise retain low-frequency,
130rate-limited polling. When the budget alert fires, stop non-critical refreshes
131and surface the age of the last verified result. Do not use cached or partial
132data to make a lease, payment, or safety decision without operator review.
133
134## Resources
135
136- [AppFolio Stack APIs](https://www.appfolio.com/stack/partners/api)
137- [AppFolio Engineering Blog](https://engineering.appfolio.com)
138
139## Next Steps
140
141See `appfolio-performance-tuning`.