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.
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.
The job’s structure
A typical nightly sync job has four phases:JavaScript
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.- 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_failurescounter and apaused_untilfield 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
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
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
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
Pattern 2: batch where possible
Some Virtuous endpoints accept multiple records in a single request. The most relevant for sync workloads:POST /api/Tag/Bulkfor tag application across multiple Contacts.POST /api/ContactNote/Bulkfor note creation.
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.
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
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
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
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.
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.
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.