Skip to main content
Event-driven sync (the architecture in Sync External Donations into Virtuous and most integration recipes) is the default recommendation for most partner integrations. But event-driven sync depends on the source platform supporting webhooks, the customer’s environment supporting persistent webhook receivers, and the data freshness requirement justifying always-on infrastructure. When any of those don’t hold, nightly sync — a scheduled batch job that pulls changes from the source platform and pushes them to Virtuous — is the right architecture. This recipe covers the full nightly sync pattern: when to choose it, how to structure the job, how to handle interruption and retry, and how to monitor it.

When nightly is the right choice

Most importantly: nightly sync is not a worse architecture than event-driven. It’s a different tradeoff. The right pattern is the one that matches the customer’s actual operational needs.
Do not choose nightly sync because event-driven seems hard. Event-driven is the right choice for most integrations because the freshness benefit is substantial for the customer’s day-to-day operations. Choose nightly only when one of the signals above genuinely applies.

Architecture

Five components:
  • Job scheduler — cron, Kubernetes CronJob, AWS EventBridge, or whatever scheduling primitive your environment provides.
  • Sync job — a single binary or script that performs the full sync.
  • Source platform read — pull changes since the last checkpoint.
  • Virtuous write — apply changes via the appropriate write endpoints.
  • State store — persistent storage for the checkpoint timestamp and any per-record sync metadata.
A nightly sync is simpler than event-driven because it runs in one place at one time. It’s also more demanding because that one run needs to handle the full hour-long (or longer) window in which something might go wrong.

The job’s structure

A typical nightly sync job has four phases:
JavaScript
The four phases are independent — phase boundaries are natural retry points if the job is interrupted mid-run.

Phase 1: checkpoint and prerequisites

The checkpoint is the heart of incremental nightly sync. It tracks where the last successful run stopped so the current run knows where to start.
Two things the checkpoint stores:
  • The highest source timestamp processed. Use this as the floor for the next run’s source query — pull everything modified after this value.
  • Failure metadata. A consecutive_failures counter and a paused_until field implement a simple circuit breaker: after N failed runs, pause the schedule until manual intervention.

Why timestamps, not “since last run started”

The checkpoint should track the highest source-side modification timestamp processed, not the wall-clock time when the last run started. The latter would miss any source-side changes that happened during the run itself. The former guarantees a record modified during the run will be picked up by the next run.
JavaScript
The source platform’s pagination shape varies — some use cursors, some use page numbers, some use since + until ranges. Adapt the loop to the source’s API.

Confirming prerequisites

Before doing any read or write, validate that the conditions for a successful run hold:
JavaScript
Early failure here saves the cost of partial runs.

Phase 2: read changes from source

The source-read pattern depends on the source platform’s API. Three common shapes: For most modern APIs, the modification-timestamp filter is what’s available. The cursor pattern is more efficient when the API supports it. Full snapshot is the fallback when neither is available — it’s expensive but works.

The full snapshot fallback

JavaScript
The cost is the storage of the previous snapshot and the comparison time. For sources with tens of thousands of records, this is acceptable nightly; for millions, it’s not.

Phase 3: apply changes to Virtuous

The write phase submits each source change as the appropriate Virtuous operation. Three patterns matter at batch scale:

Pattern 1: throttle to stay within rate limits

The Virtuous rate limit is 1,500 requests per hour per organization — see Rate Limits. For a nightly sync with thousands of changes, throttle the submission rate:
JavaScript
At 1,200 requests/hour (20% headroom), a job processes 1,200 changes per hour. A 10,000-change run takes roughly 8.3 hours — typically fitting in the overnight window. For larger workloads, raise the throttle closer to the limit (1,400/hour leaves 7% headroom). Don’t run at the cap — a single rate-limited request stops the run mid-stream until the limit resets.

Pattern 2: batch where possible

Some Virtuous endpoints accept multiple records in a single request. The most relevant for sync workloads:
  • POST /api/Tag/Bulk for tag application across multiple Contacts.
  • POST /api/ContactNote/Bulk for note creation.
Single-record endpoints (POST /api/Contact/Transaction, POST /api/v2/Gift/Transaction) are the more common case and don’t support batching.

Pattern 3: separate retryable from permanent failures

Just like the event-driven submitter (see Sync External Donations):
  • Retryable (5xx, 429, network error): re-queue for the next nightly run.
  • Permanent (400, 422): log and surface for human investigation. Do not retry on the next run.
The difference from event-driven sync is the retry cadence — nightly retries are 24 hours apart, not minutes apart. For genuinely transient failures this is usually fine; for failures that look transient but are actually permanent (a misconfigured field that produces 422 every time), the slower cadence makes the misdiagnosis cheaper.

Phase 4: persist checkpoint and emit report

The checkpoint update commits the run’s progress. If anything fails after the writes succeed but before the checkpoint is updated, the next run will re-process the same changes — your idempotency layer needs to handle this (see Idempotency and Safe Reprocessing).
JavaScript
The run report is operational visibility — a record of what happened that an on-call human can read:
JavaScript

Handling interruption

Nightly jobs are vulnerable to interruption: the scheduler kills the job after a timeout, the host machine restarts, the network drops mid-run. Make the job resumable. The pattern: persist progress within the job, not just at the end:
JavaScript
A killed job restarts and resumes from the last persisted index rather than starting over. For very long-running jobs, persist progress more frequently. The tradeoff: more frequent persistence means lower replay cost after interruption but higher steady-state I/O. Every 100 records (or every 30 seconds) is a reasonable default.

Circuit breaker

If the sync fails repeatedly, the wrong response is to keep retrying every night — that just produces more failure noise. Build a circuit breaker:
JavaScript
After three consecutive failures, the sync pauses for 24 hours. An ops human must investigate, fix the root cause, and manually clear paused_until to resume. This prevents “sync has been failing for three weeks but nobody noticed” scenarios.

Multi-tenant scheduling

For partner integrations serving many customers, run a separate scheduled job per customer. The patterns to follow:
  • Stagger start times. Don’t run all customers’ syncs at midnight; spread them across the overnight window. This isolates rate-limit budgets and keeps any single Virtuous account from being hammered by your infrastructure.
  • Per-customer state. The checkpoint, credentials, and run report are scoped by customer_id.
  • Per-customer credentials. Each customer has their own Virtuous API token and their own source-platform credentials, loaded from secrets manager.
  • Per-customer alerts. A failure in one customer’s sync should alert ops about that customer specifically, not as part of a generic “sync failed” notification.
A typical setup: a cron expression that fires once per hour, each invocation processing the customers whose scheduled time slot has arrived. This naturally staggers the load.

Combining nightly with event-driven

A common hybrid pattern: event-driven sync for resources with webhook support, nightly sync for resources without. For example:
  • Event-driven: gifts (from Stripe), contacts (from Stripe), webhook updates from Virtuous.
  • Nightly: marketing platform subscriber sync (no webhooks), data warehouse export, accounting reconciliation.
The two pipelines are independent — different schedules, different code paths, different alerting. Just make sure they share idempotency keys for any resource they both touch, so a nightly run that overlaps with an event-driven write doesn’t produce duplicates.

Monitoring

Track these metrics on a nightly sync: Most nightly sync issues show up first as a duration regression — the job is doing more work than expected and starts spilling out of its window. The second-most-common issue is checkpoint staleness, which a simple “has the sync run successfully in the last 24 hours?” check catches quickly.

Production readiness checklist

  • Checkpoint persisted on every successful run, including the highest source timestamp processed.
  • Source read uses an incremental filter (modification timestamp or cursor), not full snapshots, unless the source API requires it.
  • Virtuous writes throttled below the 1,500/hour rate limit (1,200/hour or lower recommended).
  • Retryable vs. permanent failures are distinguished — retryable changes are re-queued for the next run.
  • The run is resumable: progress persisted within the job so an interrupted run continues where it stopped.
  • Circuit breaker pauses the sync after consecutive failures and alerts ops.
  • Multi-tenant: per-customer state, credentials, and run reports.
  • Run reports persisted and inspectable by ops.
  • Monitoring alerts on missed runs, elevated failure rates, and rate-limit pauses.
  • Idempotency layer ensures duplicate records aren’t created if a run partially completes and re-runs.

Where to go next

Sync External Donations into Virtuous

The event-driven alternative — preferred when the source platform supports webhooks and freshness matters.

Constant Contact to Virtuous CRM

A real-world hybrid (some events via webhook, some via polling) that uses pieces of this nightly pattern.

Reconcile Failed Syncs

The reconciliation pattern complements both nightly and event-driven sync as a safety net.

Rate Limits

The constraint that drives the throttling pattern in Phase 3.
Last modified on May 21, 2026