Skip to main content
The Volunteer API is read-heavy and pagination-bound (15 records per page, partner cannot configure). For partner integrations of any meaningful scale, performance is determined by how cleverly you avoid unnecessary requests — not by how fast each request is. A well-cached integration can serve 100,000 users on the same rate budget that a naive integration uses for 5,000. This page covers the patterns that make Volunteer integrations performant in production: caching, request consolidation, parallelization, and the specific Volunteer quirks (small page size, no conditional requests, embedded-data endpoints) that shape the right approach.

The fundamental constraint

Page size on GET /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)
For Forms, observed-change invalidation looks like:
JavaScript
The next access to that Form refetches and re-populates the cache.

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
This eliminates N+1 patterns for common schedule reads.

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:
  • participations on User Detail include the basics but not the Project name
  • participants on Project Date Detail include the User basics but not the User’s full profile
For workflows that need the other resource’s full detail, you still need separate fetches. But for the common cases (showing schedule on a Project, showing participants on a Date), embedded data is enough.

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
The 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
The throttled client paces requests at the global rate; the parallelism just keeps the pipeline full.

Pattern 4: shorten polling cycles with updated_after

Every list endpoint that supports updated_after should use it. Without:
With:
The rate budget saved is the difference between “full re-read” (~660 requests) and “what changed” (~3-5 requests). For partner integrations serving many customers, this is the difference between a sustainable cost model and rate-limit collisions.

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
This prevents the polling worker from doing a full read accidentally if the checkpoint is at epoch.

Pattern 5: pre-compute aggregations

For reporting workloads, raw API queries are too slow at scale. Pre-compute the answers:
A dashboard query that would otherwise need to walk thousands of participation records hits this view in milliseconds.

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
Total request count: list pagination + N detail fetches. For 10k users, that’s ~10,000 detail requests on top of the list pagination.

Alternative 1: use list shape if it’s enough

JavaScript
If you only need the list-shape fields (basic profile, IDs, timestamps), skip the detail.

Alternative 2: detail only for changed records

JavaScript
The polling pattern is itself an answer to N+1 — process only what changed.

Alternative 3: parallel detail fetches with bounded concurrency

JavaScript
For workloads where detail is unavoidable, parallelism cuts wall-clock time substantially (5x with concurrency=5).

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
The warming happens once per startup; subsequent requests hit the cache.

Selective warming

Don’t warm everything — only data that:
  • Is small enough to cache in memory
  • Changes infrequently
  • Is accessed frequently
Forms, Certificates, and Organizations are the typical candidates. Don’t warm Users (too many) or Project Dates (too many for active customers).

Pattern 8: estimate before bulk operations

Before kicking off a large operation, estimate its cost:
JavaScript
Surface the estimate to the customer during onboarding (“Initial sync will take approximately 30 minutes”). Avoid scheduling backfills that exceed the API’s rate limit or your worker’s runtime.

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
Per-customer budgets prevent one customer’s runaway integration from exhausting the shared rate budget.

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
The cost of “real-time” sync (in API requests, infrastructure, complexity) is rarely worth the actual freshness improvement. Push back on this requirement:
“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 patterns combined: 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.
Last modified on May 22, 2026