The fundamental constraint
Page size onGET /users (and most list endpoints) is 15 records, not partner-configurable. Concretely:
The rate budget is not unlimited. Naive integrations that re-read all users every poll cycle quickly exhaust their budget for nothing — most of those records didn’t change.
The single most valuable performance practice: use
updated_after filters religiously. A poll that returns 50 actually-changed users beats a poll that returns 10,000 unchanged ones.
Pattern 1: cache what doesn’t change often
A few resource types in Volunteer change infrequently and benefit massively from caching:
A reference cached fetcher:
JavaScript
Per-customer keys
Cache keys must include the customer ID. Two customers may have Forms with the same ID (different VOMO orgs); cross-customer cache hits would produce wrong data.When to invalidate
The cache invalidates either:- On a TTL boundary (most common — simple and safe)
- On observed change (e.g., when polling detects the resource was modified)
JavaScript
Pattern 2: use embedded data instead of separate fetches
The Volunteer API embeds related data in several endpoints. Use the embeddings rather than separate fetches.Project Detail embeds schedule
GET /projects/{id} returns the Project with all_dates[] and next_date already populated. Don’t separately fetch Project Dates for schedule data — they’re already there.
JavaScript
User Detail embeds participations
GET /users/{id} returns the User with participations[] already populated. Don’t separately fetch participations — they’re embedded.
JavaScript
Project Date Detail embeds participants
GET /projects/date/{id} returns the Project Date with participants[] already populated.
JavaScript
When NOT to use embedded data
The embedded data is summary-shape, not full-resource-shape. For example:participationson User Detail include the basics but not the Project nameparticipantson Project Date Detail include the User basics but not the User’s full profile
Pattern 3: batch with parallelism (carefully)
Sequential requests are slow. Parallel requests are faster but can spike rate limits. The middle ground: bounded parallelism.JavaScript
concurrency parameter caps simultaneous in-flight requests. For Volunteer, 3-5 is a reasonable upper bound — higher rates the risk of triggering rate limits.
Combining parallelism with throttling
Combine bounded parallelism with the throttled client pattern:JavaScript
Pattern 4: shorten polling cycles with updated_after
Every list endpoint that supports updated_after should use it. Without:
Initial sync as the exception
The first poll for a new customer doesn’t have a checkpoint — it must read everything. Schedule the initial backfill explicitly (during onboarding) and avoid blocking the polling worker on it:JavaScript
Pattern 5: pre-compute aggregations
For reporting workloads, raw API queries are too slow at scale. Pre-compute the answers:Refresh cadence
Don’t refresh too frequently — refreshing a 100k-row materialized view every minute is more expensive than the underlying queries it’s accelerating.
See Report on Volunteer Hours for the full pattern.
Pattern 6: avoid N+1 patterns
The classic API anti-pattern: list N items, then fetch each one’s detail. Volunteer’s small page size makes this especially expensive.N+1 in practice
JavaScript
Alternative 1: use list shape if it’s enough
JavaScript
Alternative 2: detail only for changed records
JavaScript
Alternative 3: parallel detail fetches with bounded concurrency
JavaScript
Pattern 7: warm caches on startup
For partner integrations with high-frequency reads (portals, dashboards), cold caches at startup cause spike-in-traffic patterns. Warm them:JavaScript
Selective warming
Don’t warm everything — only data that:- Is small enough to cache in memory
- Changes infrequently
- Is accessed frequently
Pattern 8: estimate before bulk operations
Before kicking off a large operation, estimate its cost:JavaScript
Pattern 9: monitor cost over time
What gets measured gets managed. Track:
A monthly report that shows “Customer X consumed 850K requests last month, of which 95% were User polling, 60% returned zero changes” tells you exactly where to optimize.
Per-customer budgets
For multi-tenant partner integrations:JavaScript
Pattern 10: avoid the “real-time” tax
Customers often request “real-time” sync. Most of the time:- “I want to see new volunteers as soon as they sign up” → 15-minute polling is fine
- “I need participation data immediately after a shift” → reconciliation within an hour is fine
- “Dashboards should reflect current state” → 5-minute cache TTL is fine
“We can poll every 15 minutes, which means new volunteers appear in your dashboard within 15 minutes of signing up. We can poll every 5 minutes, which makes that ~5 minutes — but uses 3x the API requests. Is the latency difference worth the cost?”Most customers, when forced to articulate, accept 15-30 minute polling. The few who genuinely need sub-minute reactivity often have other architectural needs (real-time UI, websockets, etc.) that polling can’t satisfy regardless.
A reference performance-aware integration
A summary of the patterns in action:JavaScript
The result: a polling cycle that consumes maybe 5-10 requests per cycle for a customer with little activity, vs. 667+ for a naive integration.
Where to go next
Error Recovery Patterns
The resilience patterns that pair with performance optimization.
Rate Limits
The rate-limiting patterns these performance practices coexist with.
Data Modeling
The data model that enables fast queries.
Sync Architecture Patterns
The broader architectural patterns these practices fit into.