
Salesforce data reaches Snowflake through a one-time export, scheduled API polling (self-built or managed), Snowflake's Openflow connector, Change Data Capture events, or zero-copy sharing for Salesforce Data Cloud customers. Salesforce exposes no database log, so every method uses its APIs. For many objects kept current, the usual pattern loads each object with Bulk API 2.0, polls queryAll for records whose SystemModstamp moved, and merges them into Snowflake on Id.
The hard parts are the org's daily API allocation, which Bulk API 2.0 also draws on, and changes a timestamp cursor handles badly: formula values and deletes. Estuary is one managed option for this route: its Salesforce connector backfills through Bulk API 2.0 where it can, polls REST queryAll for changes on a per-object interval, and loads Snowflake on a configurable schedule.
Choosing a Salesforce to Snowflake method
| Approach | Best fit | Freshness | Operational burden | Example implementation |
|---|---|---|---|---|
| One-time export | Migration or snapshot | Point in time | Low, manual | Data Loader or a Bulk API 2.0 job → COPY INTO |
| Polling you build | A few objects, an existing orchestrator | At the schedule | High: cursors, MERGE, formula re-reads | Airflow or cron calling Bulk API 2.0 and REST |
| Snowflake-native connector | Snowflake-only, standard login host | Every 1 minute to 24 hours | Medium: Openflow runtime | Openflow Connector for Salesforce Bulk API |
| Change Data Capture events | Event-level reaction on a few objects | Pushed to a subscriber; Snowflake still batch-loads | High: subscriber, backfill, merge layer | Pub/Sub API client |
| Zero-copy sharing | Salesforce Data Cloud customers | Shared, not copied | Low, given Data Cloud | Data Cloud zero-copy integration |
| Managed polling connector | Many objects, no pipeline code | Poll interval plus load schedule | Low: configuration | Estuary |
Three of these methods poll Salesforce, so most of this guide is about polling well.
How Salesforce to Snowflake API replication works
A poller loads each object once, then repeatedly fetches records changed since a saved cursor and merges them by Id.
- Initial load: a Bulk API 2.0 query job per object, which Salesforce recommends above 2,000 records. Bulk jobs can't select compound address or geolocation fields.
- Cursor:
SystemModstamp, which moves on user and automated updates.LastModifiedDatereflects only users. - Change sweeps: REST
queryAllover aSystemModstampwindow, up to 2,000 records per response, including deleted and merged records. Below 2,000 records, one call beats a Bulk job's submit, poll and download requests. Changes are then staged and merged into a Snowflake table per object.
Salesforce requirements
The daily API allocation belongs to the org and is shared by every integration and installed managed package:
| Edition | Daily API requests |
|---|---|
| Enterprise, or Professional with API access | 100,000 + 1,000 per Salesforce license |
| Unlimited, Performance | 100,000 + 5,000 per Salesforce license |
| Developer | 15,000 |
Bulk API 2.0 requests count toward it, and query jobs have their own caps: 10,000 jobs and 1 TB of results per rolling 24 hours.
The integration user needs:
- API Enabled, from its profile or a permission set.
- Read on each object and field-level security on each field. API clients see only what the user can.
- View All Data to replicate every record, because API calls respect the sharing model.
- Your My Domain host for sign-in, such as
mycompany.my.salesforce.com, if login policy blockslogin.salesforce.com.
Snowflake requirements
Give the pipeline a database, an X-Small warehouse, a key-pair service user (Snowflake is phasing out passwords for service users) and one schema per Salesforce org, so sandbox data never sits beside production.
GROUPandORDERare reserved in Snowflake andACCOUNT,CASEandORGANIZATIONrestricted, so quote those table names, which makes them case-sensitive ("Case"). KeepQUOTED_IDENTIFIERS_IGNORE_CASEatFALSEso they resolve.- Salesforce date/time values are UTC, so
TIMESTAMP_LTZor a UTC-normalizedTIMESTAMP_NTZloses nothing. AUTO_SUSPEND = 60bills each load about a minute of warehouse time, not the ten minutes the default leaves it running.
Setting up Salesforce to Snowflake replication
- Create the integration user, and check headroom in the REST
/limitsresource (DailyApiRequests,DailyBulkV2QueryJobs). - Prepare Snowflake and choose objects, including the parents your joins need.
- Load existing data with a Bulk API 2.0
queryAlljob per object, paging large objects byId(WHERE Id > :last_id ORDER BY Id LIMIT n). - Sweep changes over a closed window that ends a few minutes before now, and merge on
Id:
sqlSELECT Id, Name, StageName, Amount, IsDeleted, SystemModstamp
FROM Opportunity
WHERE SystemModstamp > 2026-10-06T10:00:00Z
AND SystemModstamp <= 2026-10-06T10:25:00Z
ORDER BY SystemModstamp ASC- Schedule formula re-reads, and pick a load interval that matches how fresh reports really need to be.
- Validate. Compare record counts. Edit a record, delete one, add a custom field, and change a parent value behind a cross-object formula; check each after one load (the formula after its re-read). After a day, compare API usage with the allocation.
What happens after the initial load?
- Updates, automated ones included, merge by
Id. - Deletes and merges:
queryAllreturns deleted records and the losing records of merges withIsDeleted = true, plus archivedTaskandEventrecords. The rows stay in Snowflake unless the pipeline removes them. - Purged records vanish from every API. The Recycle Bin keeps records for 15 days, up to 5,000 per license; when it's full, Salesforce purges the oldest that have been there two hours, so an infrequent poll can miss a mass delete.
- Formula fields are calculated on read. When a formula reads a parent record, editing the parent moves the parent's
SystemModstamp, not the child's, so Snowflake keeps the child's stale value. Re-read formula columns on a schedule, or rebuild simple formulas as Snowflake views. - New custom fields arrive once the object is re-described and the user can see them. Records loaded before the field existed keep
NULLthere until the object is re-read.
Salesforce to Snowflake data type mapping
| Salesforce field | API type | Snowflake target | Notes |
|---|---|---|---|
| Id, Lookup, Master-Detail | ID | VARCHAR | |
| Multi-select picklist | string | VARCHAR | Semicolon-delimited; split with SPLIT_TO_TABLE |
| Checkbox | boolean | BOOLEAN | |
| Currency, Percent, decimal Number | double | NUMBER(p,s) | FLOAT if mapped by API type |
| Date, Date/Time, Time | date, dateTime, time | DATE, TIMESTAMP_*, TIME or VARCHAR | Date/Time values are UTC |
| Address, Geolocation | compound Address, Location | VARIANT, or the component columns |
Currency and percent fields arriving as FLOAT
Salesforce exposes currency, percent and decimal number fields as double, so type-mapping loaders create FLOAT columns, which hold about 15 significant digits: totals pick up floating-point error, and long values round. Describe reports each field's precision and scale, so cast to NUMBER(precision, scale) in a view or at load. Bulk API 2.0 returns CSV, and a loader with no type mapping makes every column VARCHAR.
Performance, latency and Snowflake cost
A Salesforce change waits up to a poll interval, any safety lag and a Snowflake load interval. Polls spend API requests; loads spend warehouse credits, and Snowflake bills at least a minute each time a load wakes the warehouse, so loading every few minutes means it never suspends. Neither makes Snowflake tables real time: even Change Data Capture events, pushed as changes happen, wait for the next load.
Where the API requests go
A poller's floor is objects × polls per day × requests per poll. Twenty objects polled every 5 minutes, with one describe and one query each, use 11,520 requests a day, a tenth of an Enterprise org with 15 licenses (115,000). Add extra queries for wide objects (REST caps the URI and headers at 16,384 bytes), a request per further 2,000-record page, every Bulk submission, status check and result page, and formula re-reads of every record.
To spend less, poll only as often as you need, sync only objects you query, hide unused fields from the integration user, and re-read formulas daily. Usage shows in System Overview, the Sforce-Limit-Info header and /limits.
Production considerations
- Recovery. A restarted poller resumes from its cursor, and the merge absorbs the overlap. Rows for records purged during a long outage stay until you compare
Idlists. - Backfills. A re-load spends allocation and Bulk jobs again unless the pipeline keeps its own copy, and a merge-based re-load never removes rows for purged records.
- Monitoring. Alert on API usage, cursor age per object, failed Bulk jobs and time since the last Snowflake load.
- Security. One read-only integration user per pipeline, and key-pair auth on Snowflake.
Common problems and troubleshooting
Fields or records missing in Snowflake
Run the object's describe (GET /services/data/vXX.X/sobjects/Account/describe) as the integration user. A field absent from the response is hidden by field-level security (Set Field-Level Security in Object Manager). Short row counts point at sharing: grant View All Data, then re-load the object, since the cursor has already passed those records.
ContentDocumentLink queries rejected
Salesforce rejects ContentDocumentLink queries that don't filter on Id, LinkedEntityId or ContentDocumentId, and a replication sweep filters on SystemModstamp. From API version 59.0, the Query All Files permission, which requires View All Data, lifts the rule. Otherwise leave the object out of the sync.
Millions of records with one SystemModstamp
A mass update stamps millions of records with one instant. OFFSET paging fails past 2,000 rows (NUMBER_OUTSIDE_VALID_RANGE), and a cursor saved in the middle of a tie skips the rest of it after a restart. Page by Id inside the window, follow nextRecordsUrl instead of OFFSET, and advance the cursor only when the timestamp changes.
Bulk API daily caps reached
Jobs fail with Max bulk v2 query jobs (10000) per 24 hrs has been reached or, past 1 TB of results, Max bulk v2 query result size stored (1000000000) kb per 24 hrs has been exceeded. Both windows roll over 24 hours (DailyBulkV2QueryJobs in /limits). Load urgent objects over REST meanwhile.
Using Estuary for Salesforce to Snowflake
The Salesforce capture connector feeds the Snowflake materialization: each object becomes a collection keyed on /Id, and each collection merges into its own Snowflake table.
- API usage: sweeps always use REST
queryAll; each object, every interval (5 minutes by default), costs one describe plus one query per field chunk. Backfills use Bulk API 2.0, except for objects with compound address, geolocation or base64 fields (Account,Userand others), which backfill over REST. - Deletes: a delete arrives as an update with
IsDeleted = true. The row stays, and Hard Delete doesn't remove it, so filter withWHERE NOT ISDELETED. Records purged from the Recycle Bin before a sweep keepIsDeletedfalse. - Formula fields: a per-binding cron (default 23:55 UTC daily) re-reads them and merges them into stored records. Keep standard updates: a delta-update table gets each refresh as a row of
NULLs around the formula values. - Types: custom fields are captured as strings, since Salesforce's describe often mistypes them: custom numbers become
FLOATorINTEGERfrom a format hint, custom checkboxesTEXT. Standard currency and percent becomeFLOAT, and compound addressesVARIANTbeside their component columns. Map collections to tables below shows how to keep amounts exact. - Limits: standard objects come from an internal list (support adds more); external (
__x) and big (__b) objects aren't discovered. Loads follow the sync schedule, 30 minutes by default.
Implementing Salesforce to Snowflake with Estuary
These steps adapt Estuary's general data flow guide to this route.
- Check the prerequisites. The Salesforce and Snowflake requirements above apply; for Estuary you also need:
- A read-only Salesforce user, plus an External Client App for Client Credentials.
- A Snowflake account prepared with the connector's setup script, its service user holding a registered public key (key-pair authentication).
- Create the Salesforce capture. In Sources, New Capture opens the connector list; choose Salesforce, give the capture a name, and complete these endpoint properties:
- Authentication: OAuth through Authenticate your Salesforce account; a username, password and security token, which fails under enforced SSO; or Client Credentials with a Consumer Key and Consumer Secret.
- My Domain: your login host, such as
mycompany.my.salesforce.com. Client Credentials requires it. - Sandbox: on for a sandbox org.
- Start Date: skips records whose cursor timestamp, usually
SystemModstamp, is older.
- Select objects. Click Next. Output Collections lists the supported objects, with
Account,Contact,Lead,Opportunity,Case,Task,Userand others enabled; add what else you need, such asOpportunityLineItemor custom__cobjects. Per binding:- Interval: the poll interval,
PT5Mby default. - Formula Field Refresh Schedule: a UTC cron,
55 23 * * *by default. Clear it where formulas don't matter.
- Interval: the poll interval,
Click Save and publish. Each enabled object's backfill begins, and its change sweeps run in parallel from the start.
- Create the Snowflake materialization. The published capture's page has a Materialize button; choose Snowflake from the connector list, then complete these endpoint properties:
- Host (Account URL) plus the Database, Schema, Warehouse and Role you created for this org.
- Snowflake Timestamp Type: no default;
TIMESTAMP_NTZ (normalize to UTC)orTIMESTAMP_LTZkeeps Salesforce's UTC times. - Authentication: Private Key (JWT), entering the service user as User and its unencrypted PKCS#8 key as Private Key. The User Password tab is deprecated.
- Hard Delete: leave off; it ignores
IsDeletedupdates.
- Set the load schedule.Sync Frequency, under Sync Schedule, is 30 minutes by default and sets how often the warehouse wakes; the binding Interval sets how often Salesforce is polled. If half-hour-old data is fine, a
PT30MInterval needs a sixth of the default's sweeps. A fast-sync window (Timezone, Fast Sync Start Time, Fast Sync Stop Time, Fast Sync Enabled Days) keeps the short frequency to business hours, with 4-hour loads outside. See sync schedules.
- Map collections to tables. Click Next, pick a naming convention, and leave Delta Updates off for each binding under Source Collections. Reserved-word objects get quoted names, such as
"Account". To keep currency and percent fields exact, add aDDLoverride that matches the field's describe precision and scale under its binding'sfields.includein the specification editor before publishing, such asAmount: { DDL: "NUMBER(18,2)" }(custom column types); it applies when the table is created. Custom number fields arrive as the text Salesforce sent, socastToString: truekeeps those digits exactly in aTEXTcolumn. - Publish. Click Save and publish. Estuary creates a table per object, and the backfill lands immediately, because backfills ignore the sync schedule.
- Check the first loads.
- The capture details page reads PRIMARY in its Shard Information card; open an object's collection from the Collections list to see records in its Data Preview.
- On the materialization details page, Data Read rises as backfills land.
- In Snowflake, compare
SELECT COUNT(*) FROM "Account" WHERE NOT ISDELETEDwith Salesforce, then run the validation checks.
How Estuary handles Salesforce API budgets, formula refreshes and rebuilds
Id-paged backfills and tie-safe sweeps
Since July 2026, backfills page each object by Id, so their speed no longer depends on how records spread over time. Each page is one Bulk job of up to 500,000 records, downloaded 25,000 per request; a page Salesforce rejects (compound fields, an unsupported object, a daily cap) moves to REST instead of failing. Sweeps stop 5 minutes short of now, so late-visible records aren't skipped, and the cursor advances only to a later timestamp, so ties survive restarts.
Formula refreshes as partial documents
A refresh selects only Id, the cursor and formula fields, skips objects without formulas, and merges each result into the stored record, so other columns keep their values. Each run still re-reads every record, in Bulk jobs that count toward the daily caps.
Rebuilding Snowflake without calling Salesforce
Each object's records persist in a collection, written as JSON files to your own cloud storage bucket. A materialization backfill reloads Snowflake tables from those files, and a second destination reads them too, with no Salesforce requests. A capture re-read spends requests and can't remove rows for records purged meanwhile, because incremental objects never emit deletes. Estuary's trial buckets drop data after about 20 days, so point collections at your own bucket to keep full history.
When another approach may make more sense
- Snowflake-only and native: the Openflow Connector for Salesforce Bulk API refreshes every 1 minute to 24 hours by
SystemModstamp. It ignores address and location fields, doesn't support custom Salesforce domains, turns formulas into Snowflake views, and only soft-deletes (isDeleted). - Salesforce Data Cloud customers: Snowflake recommends its zero-copy integration.
- Event-level reaction on a few objects: Change Data Capture through the Pub/Sub API. By default an org selects up to 5 entities and gets 25,000 event deliveries per 24 hours on Enterprise (50,000 on Unlimited and Performance), shared with high-volume platform events and counted per subscriber. Events are kept 72 hours and updates leave unchanged fields empty, so you still build a backfill and a merge layer. Estuary's connector polls and doesn't cover this.
- One migration, or a few objects nightly: a Bulk API 2.0 export with
COPY INTOandMERGE, re-reading whole objects each run. Estuary's connector can also poll on a long Interval.
Related resources
- Estuary: Salesforce connector and its formula fields and field permissions notes, Snowflake materialization, sync schedules
- Salesforce: API request limits, Bulk API 2.0 limits,
queryAll, system fields, Change Data Capture allocations - Snowflake: Openflow Connector for Salesforce Bulk API, reserved keywords
To try it, follow Estuary's data flow guide or create a Salesforce capture in the Estuary dashboard. Pricing: estuary.dev/pricing.
FAQs
Can I replicate Salesforce field history to Snowflake?
How do I combine Salesforce data with other sources in Snowflake?

About the author
Seán Whelan is a Solutions Engineer at Estuary, helping customers design, troubleshoot, and run production data pipelines. Seán works across change data capture, database replication, and SaaS integrations, and writes about what actually comes up when real teams move data.

















