The two constraints that matter
Two constraints shape almost every performance decision:
Most performance issues partner integrations hit are about one of these two. The patterns below all map back to handling them.
See Rate Limits for the detailed rate-limit reference and Transactions for the holding-state model.
Prefer webhooks over polling
The single highest-leverage performance decision: use webhooks for change detection wherever Virtuous publishes events for the change.
A polling-only sync that runs every 15 minutes against
POST /api/Contact/Query and POST /api/Gift/Query consumes ~400 requests/day per resource — almost 8% of an hour’s budget burned on basic change detection. The webhook equivalent costs zero from your budget.
Use polling as a reconciliation backstop, not as the primary signal. See Webhooks Overview and Reconcile Failed Syncs.
Choose the right response shape
Most read endpoints have multiple response shapes — abbreviated and full. Choose deliberately.For Contact Queries
FullContact is meaningfully slower per request. For a 10,000-Contact result set, the difference can be the cost of an hour of clock time and several hundred extra requests on the rate-limit budget.
Pattern: abbreviated query + targeted full fetch
For workflows where you process the abbreviated result and only need full detail for a small subset:JavaScript
FullContact query over the entire set.
Paginate with the right strategy
The skip/take pattern works for small to medium result sets. For very large or unstable result sets, ID-cursor pagination is better.Skip/take
JavaScript
- At high
skipvalues, server-side query performance degrades. A request withskip=50000is slower thanskip=0. - If records are inserted while paginating, the offsets shift — you can re-process or miss records.
ID-cursor pagination
JavaScript
- Each query is bounded — no
skipoverhead at high offsets. - Stable under concurrent inserts: a record inserted with a new ID lands in a later page, not in a page you’ve already processed.
- Naturally resumable: persist the cursor between batches and resume from where you stopped.
Contact Id (or equivalent ID parameter) be an indexed filter on the Query endpoint.
See Query Contacts by Filters — resumable exports for the full pattern.
Maximize take for bulk operations
Every paginated read endpoint accepts a take parameter capped at 1,000. Use the cap for bulk operations.
For interactive UIs showing a few records at a time, smaller
take values are fine. For sync, exports, and reconciliation, always use 1,000.
Filter aggressively in the request, not client-side
A common partner integration anti-pattern: pulling broad result sets and filtering on the client side. Two costs:- More requests. Each unnecessary record paginated is rate-limit budget consumed.
- More data transferred. The full response payload for records you discard is wasted bandwidth.
JavaScript
Cache reference data aggressively
Some data changes rarely and is referenced often. Cache it.What’s cacheable
What’s not cacheable
Implementation pattern
JavaScript
Run requests concurrently, but bounded
If you have a hundred records to update, you don’t need to do them sequentially. But you also can’t fire all hundred concurrently — that bursts the rate limit and your error rate spikes. The pattern: a bounded concurrency limit, typically 4–8 concurrent in-flight requests.JavaScript
Use a token bucket for steady-state pacing
For continuous workloads (incremental sync, ongoing reconciliation), a token bucket smooths request dispatch to fit the rate limit while still allowing modest bursts:JavaScript
Reuse HTTP connections
Every TCP and TLS handshake adds latency. Reusing connections via HTTP keep-alive eliminates that cost. In Node.js with the global fetch, connection pooling is automatic for the same origin. In other runtimes:- Java/Apache HttpClient: configure a connection pool with appropriate
maxConnPerRoute. - Python/requests: use a
Session()object rather than module-levelrequests.get(). - Go: use a long-lived
http.Clientwith a configuredTransport. - Ruby/Net::HTTP: use a long-lived
Net::HTTP::Persistentinstance.
Defer heavy work in webhook handlers
A webhook handler should acknowledge the delivery as quickly as possible and defer processing. Heavy work inside the handler increases the risk of timeout-triggered retries — which produces duplicate deliveries you have to defend against.JavaScript
Batch where the API supports it
Most Virtuous endpoints accept one record per request. A few accept batches:
For workloads that involve many small writes — applying a “Year-End-2024” tag to ten thousand donors, for instance — these batch endpoints reduce request count by 100x or more. Always check whether a batch endpoint exists before looping over a single-record endpoint.
The CRM+ spec exposes only a small set of batch endpoints. If a workflow requires bulk operations that don’t have a batch endpoint, you may need to escalate to Virtuous engineering for either a per-organization rate-limit exception (see Rate Limits) or a feature request for a new batch endpoint.
Monitor the right metrics
Performance regressions are usually visible in one of these metrics before they become user-visible:
Most well-functioning integrations sit far below the rate limit. If you’re routinely brushing against it, the issue is usually elsewhere — too-frequent polling, overly-broad queries, or missing caching — not “the limit is too low.”
Where to go next
Error Recovery Patterns
The companion practices for handling the inevitable failures.
Rate Limits
The reference for the rate-limit budget all these patterns optimize for.
Pagination and Filtering
The mechanics of paginated reads that several patterns on this page depend on.
Build a Nightly Data Sync
A recipe that puts the throttling and pacing patterns from this page into practice.