The performance principles
Six principles, in rough order of impact:
The next sections drill into each.
Principle 1: webhooks over polling
For any change-detection workflow — syncing new gifts, reacting to donor updates, processing recurring failures — webhooks deliver the change within seconds and consume zero rate-limit budget for the detection itself. Polling at any reasonable cadence is strictly worse.The polling trap
A typical naive polling pattern:JavaScript
- 288 requests per day per customer just for change detection. For a partner with 100 customers, that’s 28,800 daily requests producing no user-facing value.
- 5-minute lag between gift creation and detection.
- Edge cases at the boundaries — gifts created exactly between polls can be missed or double-counted.
- No insight into deletions or updates unless you also poll modification timestamps.
The webhook alternative
JavaScript
- One subscription per customer, zero ongoing rate-limit cost for the detection.
- Seconds-level latency instead of minutes.
- All event types covered — creates, updates, deletes — with no extra work.
- Edge cases handled by Raise — retries, ordering, etc.
When polling is still appropriate
Three legitimate cases for polling:
For these cases, throttle aggressively and minimize request count. See Reconcile with CRM+: pattern 3 for the reconciliation pattern.
Principle 2: use the largest page size
For paginated reads, each request consumes one rate-limit slot regardless of how many records it returns. ATake=1000 request returns 1,000 records for the cost of one request; a Take=25 request returns 25.
When this matters most
For partner integrations doing bulk reads, default to
Take=1000 and only reduce if a specific endpoint enforces a lower maximum. The pagination loop is otherwise identical.
A bulk read pattern
JavaScript
Take=1000 produces 100 requests. The same read with Take=100 produces 1,000 requests. The same data, 10× the rate-limit cost.
Principle 3: cache reference data
Some Raise resources change rarely — Campaigns, Projects, MotivationCodes, Premiums, CustomFields. Caching these eliminates the majority of read traffic for analytics and reporting integrations.What to cache
A reference-data cache pattern
JavaScript
Cache invalidation strategies
For most reference data, time-based with a 1-hour TTL is the right default. The occasional stale lookup is acceptable; the rate-limit savings are substantial.
Query options caching
The QueryOptions discovery endpoint (GET /api/Query/options/{queryType}) returns the integer-to-label mapping for query operators and filter parameters. This effectively never changes — cache it at integration startup and refresh once a day at most:
JavaScript
Principle 4: push filters into the API
A query that returns 500 records after filtering is much cheaper than one that returns 50,000 records the integration then filters client-side.Filter at the source
Bad pattern — pull everything, filter in code:JavaScript
JavaScript
Combine multiple filters
Filters compose with AND semantics within a condition group. Combining narrows the result set further:JavaScript
Use /list + Filter for free-text matching
For interactive search where the user types a query string, the simple Filter parameter on /list is cheap and fast:
JavaScript
POST /api/Donor/query for what’s essentially a search-as-you-type lookup — /list with Filter handles it with less overhead.
Principle 5: use IncludeDetails=false for bulk reads
IncludeDetails=true includes related entities (addresses, contact methods, embedded donor on gifts, etc.) in each response item. For bulk reads, this can multiply the payload size 5–10×.
The Raise spec calls this out explicitly on POST /api/Donor/query:
When includeDetails=true, the response includes all related entities (DonorAddresses, DonorContactMethods) similar to the GET by ID endpoint. This may impact performance for large result sets.
Use it sparingly
Default to
IncludeDetails=false. Switch to true only when the workflow demonstrably needs the related entities and the result set is small (typically a few hundred records at most).
When you do need details on many records
For workflows that genuinely need full records across many donors, fetch them individually rather than as a bulk read:JavaScript
IncludeDetails=true, but each is small and the workload is more controlled. For very large workloads, prefer the controlled-concurrency pattern over the bulk-with-details one.
Principle 6: choose the right endpoint
Different endpoints have different costs for the same logical question. Some examples:Donor’s gifts: scoped endpoint vs. filtered query
The scoped endpoint is cheaper because the filter happens at the source rather than requiring full-table filter evaluation.
Donor lookup by email: search vs. query
/search is optimized for free-text lookups and is faster than the structured query for single-record matches.
Reading reference data: list vs. query
For unfiltered bulk reads,
/list is cheaper than /query.
Reading a single record: get-by-ID vs. filtered query
Always prefer get-by-ID when you have the ID.
The general principle: when multiple endpoints could answer the same question, choose the most specific one. Specificity translates to less work at the API and faster responses.
Avoiding common performance traps
A few specific anti-patterns that cause performance issues:Sequential fetches when parallel would do
JavaScript
JavaScript
Repeated lookups of the same data
JavaScript
JavaScript
Synchronous webhook processing
JavaScript
JavaScript
Fetching unnecessary fields with selectedColumns
When you don’t need every field, request a subset:
JavaScript
selectedColumns reduces response payload by an order of magnitude. The set of valid columns is discovered via QueryOptions.
Measuring performance in production
For partner integrations operating at scale, instrument for visibility:Track per-endpoint latency
JavaScript
Track request count per customer
JavaScript
Track cache hit rate
JavaScript
Alert on rate-limit responses
If429 responses occur (the spec doesn’t formally document them but they happen), alert immediately:
JavaScript
A performance checklist
Run through this when building or auditing an integration:- Change-detection uses webhooks, not polling, wherever possible
- Bulk reads use
Take=1000 - Reference data (Campaigns, Projects, MotivationCodes, CustomFields, QueryOptions) is cached with appropriate TTLs
- Filters are applied in the API request, not in client-side filtering of large result sets
- Bulk reads use
IncludeDetails=false selectedColumnsreduces payload when only a few fields are needed- Donor-scoped endpoints are used for donor-scoped reads (not filtered queries)
- Get-by-ID is used when the ID is known (not filtered queries)
- Concurrent fetches use a controlled-concurrency pool
- Webhook handlers acknowledge fast and process async
- Latency, request count, cache hit rate, and rate-limit responses are instrumented
Where to go next
Error Recovery Patterns
What to do when the techniques on this page meet the reality of transient failures.
Rate Limits
The throttling patterns that pair with the performance practices.
Pagination and Filtering
The reference for the pagination, filtering, and
selectedColumns mechanics used here.Sync Architecture Patterns
The broader architectural patterns these performance techniques fit into.