Your warehouse knows the lead score, the product usage trend, the LTV. Salesforce knows none of it, because nobody built the pipe. So reps work off gut feel and a "Last Activity" field, while the answer sits in a table three systems away. This is what reverse ETL exists to fix — and what makes it deceptively annoying to build yourself.
Why this is harder than it looks
Every engineer's first instinct is "it's just an upsert into an API, how hard can it be." Then they hit the actual constraints.
API limits are real and they bite. Salesforce meters you two ways: a rolling 24-hour API request allocation (roughly 1,000 calls per user license on Enterprise Edition, more on Unlimited/Performance, with an org-wide floor regardless of headcount) and a concurrency cap on long-running requests (25 concurrent for most editions). Push 50,000 rows through the REST API one record at a time and you'll blow through both before lunch. This is the single most common reason a "quick sync script" turns into a Monday morning incident.
Upsert vs. insert isn't a detail, it's the whole design. You need a stable external ID field on the Salesforce object — not the Salesforce record ID, which doesn't exist until the record does — matched against a key that also exists in your warehouse (email, a CRM ID you wrote back earlier, a hashed composite key). Get the matching key wrong and you get duplicate Contacts instead of updated ones, silently, for weeks.
Field mapping is where the schema fights back. Picklists reject values that aren't in the allowed set — no fuzzy matching, no coercion, just a failed row. Multi-select picklists want semicolon-delimited strings, not arrays. Lookups need the target record to already exist, so if you're syncing Opportunities that reference Accounts, order matters. None of this shows up until you're mapping the fifth field and Salesforce throws INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST on a value that looked perfectly reasonable in your warehouse.
Incremental vs. full refresh determines whether you sleep at night. Full refresh is simple to reason about and murders your API quota at scale. Incremental (only sync rows that changed since last run) is efficient but requires you to track state — a last_synced_at watermark, a hash of the row to detect changes, something. Skip this and you'll either re-sync everything daily or miss updates silently.
Validation rules and triggers will block writes you don't control. Salesforce admins add validation rules and Apex triggers that have nothing to do with your integration and everything to do with sales process enforcement. Your sync can pass every field-mapping check you wrote and still fail because a validation rule requires a Close Date on any Opportunity above $10k. The error comes back in the API response, but only if you're actually checking per-row results instead of assuming a 200 means everything landed.
Three ways to actually do this
1. Custom scripts (Python + simple_salesforce)
The default move for any engineer with warehouse access and an afternoon. simple_salesforce wraps the Salesforce REST and Bulk APIs cleanly, and a first version genuinely takes an hour:
from simple_salesforce import Salesforce
import pandas as pd
sf = Salesforce(username=USER, password=PASS, security_token=TOKEN)
df = pd.read_sql("""
SELECT email, lead_score, lifetime_value, last_active_at
FROM analytics.lead_scores
WHERE updated_at >= now() - interval '1 day'
""", warehouse_conn)
records = df.rename(columns={
"email": "Email",
"lead_score": "Lead_Score__c",
"lifetime_value": "LTV__c",
}).to_dict("records")
# Bulk API v2 upsert on a custom external ID field
sf.bulk2.Lead.upsert(records, external_id_field="Email")This works, and for one or two syncs it's genuinely fine. Where it stops being fine: Salesforce renames a field and the script fails on a KeyError nobody sees until a rep complains three weeks later. You add a second object to sync and now you're hand-rolling retry logic, rate-limit backoff, and dead-letter handling for failed rows. Someone leaves the company and the cron job that runs this becomes tribal knowledge. I've inherited this exact script at three different companies — it's always "temporary," it's never actually maintained, and it always breaks on a schema change nobody announced.
2. Dedicated reverse ETL tools (Census, Hightouch, Polytomic)
Purpose-built for this problem. You get a GUI for field mapping, built-in handling of Salesforce's picklist and lookup quirks, scheduling, sync observability (which rows failed and why), and support for dozens of destinations beyond Salesforce. If you're running 10+ syncs across multiple SaaS tools — Salesforce, HubSpot, Intercom, an ad platform for audience sync — this is the right category of tool, full stop.
The catch is cost and scope: Census and Hightouch both start around $350-800/month for anything beyond a trial tier, and they do one job (move data to destinations) rather than also being where you query and analyze the warehouse data in the first place. If you're comparing the two directly, we've broken down the differences in Hightouch vs. Census.
3. Integrated platforms (Fastero)
The middle path: you're already querying your warehouse for dashboards and reports, so the sync is a step in that same workflow instead of a separate system. You write the query that defines what gets synced, map it to a Salesforce object, and attach a schedule — no separate vendor, no separate bill just for the sync layer.
This makes the most sense when you have a handful of syncs (2-5, not 40) and you'd rather not run a dedicated reverse ETL vendor just to keep three fields updated on Contacts.
Fastero
Connect your database. Ask questions. Get dashboards.
Postgres, BigQuery, Snowflake, and 10+ sources — live-connected, AI-powered, no dashboard builder learning curve.
Try free →Which approach fits your situation
| Approach | Best when | Cost | Maintenance |
|---|---|---|---|
| Custom scripts | 1-2 syncs, engineering-heavy team, full control matters more than time | Dev time only | High — you own every schema change, retry, and failure mode |
| Dedicated tool (Census/Hightouch) | 10+ syncs, multiple destinations, a data team to own it | $350-800+/mo | Low — vendor handles API quirks and observability |
| Integrated platform (Fastero) | 2-5 syncs, want analytics and sync in the same tool | $20+/mo | Low — no separate vendor relationship for one workflow step |
For a broader field of options beyond these three, see our roundup of reverse ETL tools.
Step-by-step: syncing warehouse data to Salesforce with Fastero
1. Connect your warehouse. Postgres, BigQuery, or Snowflake — add the connection once, encrypted at rest.
2. Write the query that produces the sync rows. This is the whole spec for what gets pushed:
SELECT
email,
lead_score,
lifetime_value AS ltv,
last_active_at
FROM analytics.lead_scores
WHERE updated_at >= now() - interval '1 day'Keep the WHERE clause incremental from day one — filtering on a watermark column instead of pulling the full table is what keeps you inside Salesforce's API limits as the dataset grows.
3. Configure the Salesforce destination. Pick the object (Lead, Contact, custom object), map each query column to a Salesforce field, and set the external ID field used for matching — this is what makes it an upsert instead of a stream of duplicate inserts. Under the hood this runs on Bulk API v2, not the standard REST API. Bulk API v2 auto-chunks your payload, processes it asynchronously, and gives you per-batch job status instead of one request per record — it's what you actually want for anything beyond a handful of rows, and it burns far less of your daily API allocation than looping REST calls.
4. Set the trigger. Schedule it (daily is the default most teams land on) or fire it on a data-change event if you need closer-to-real-time freshness.
5. Dry-run with a row limit. Add LIMIT 20 to the query, run it against a Salesforce sandbox or a test segment, and check the job results before you point it at production. This is the step people skip and regret — it's also the one place you'll catch a picklist mismatch or a validation rule before it fails silently on 500 real Contacts.
Pitfalls that bite after it's "working"
- Validation rules failing silently. A sync that reports "job complete" isn't the same as "every row succeeded." Bulk API v2 gives you per-record results — check them, don't just check the job status.
- Syncing more often than the data changes. Hourly syncs on data that updates once a day just burns API quota and adds noise to Salesforce's audit trail. Match the sync frequency to how often the source data actually changes.
- No idempotency on retry. If a batch partially fails and you retry the whole job without upsert semantics (matching on an external ID, not blind insert), you get duplicates. This is the single most common cause of a messy Salesforce object months later.
- Field-level security blocking visibility. The sync can succeed — API user has field-level access — while the actual reps can't see the field because their profile doesn't. Nobody notices until someone asks why the lead score "isn't syncing" when it's been landing in Salesforce the whole time.
Do you actually need reverse ETL?
Be honest about scale before picking a tool.
If your sync is one daily query updating a few hundred rows — this is straightforward. A well-built integrated sync or even a careful script handles it fine. Don't over-provision.
If you need real-time sync, complex conditional field logic, or 50+ object/destination combinations — that's the point where Census or Hightouch's $350-800/month is worth it. You're paying for orchestration and observability you'd otherwise build yourself, badly.
If the only reason you want this is to answer one recurring question — "what's this account's usage trend" — you may not need reverse ETL at all. Embedding the report directly where reps already work, rather than pushing computed fields into Salesforce, is often less to maintain. If Salesforce isn't your only downstream system, the same warehouse query pattern applies to billing data too — see how this looks for Stripe.
Reverse ETL is the right tool when reps genuinely need the data inside Salesforce to do their job — not because it seemed like the modern thing to build. Start with the smallest version that answers the actual question, and only add machinery when the sync count or the freshness requirement actually demands it.
Ready to sync warehouse data to Salesforce without standing up a separate pipeline? Start free — connect your warehouse and have a Salesforce sync running in under 15 minutes. No credit card required.
Last updated: July 2026.

