Skip to content

Aurora PostgreSQL and Salesforce in Both Directions, Step by Step

Aurora PostgreSQL holds the product data and Salesforce holds the commercial record, and both get edited. This is the implementation in the order the work has to happen: the custom DB cluster parameter group, the reboot, the publication and the slot on the writer endpoint, External ID upserts through Bulk API 2.0, Change Data Capture for the return direction, and what a cluster failover does to all of it.

Author
Ruben Burdin · Founder & CEO
Published
July 23, 2026
Read time
12 min read
Aurora PostgreSQL and Salesforce in Both Directions, Step by Step
DATA ENGINEERING

Aurora PostgreSQL holds the product data. Salesforce holds the commercial record. Both get edited, and by the second month nobody can say which seat count the renewal quote was built from. Keeping the two in agreement in both directions is less a connector problem than a set of decisions: which endpoint owns the replication slot, which field carries identity across the boundary, and what happens the first time the cluster fails over.

This is the implementation in the order the work has to happen: the custom DB cluster parameter group and the reboot that turn logical decoding on, the publication and slot that can only live on the writer, the External ID that makes a Salesforce write idempotent, Change Data Capture for the return trip, and the metrics that keep a paused sync from filling the cluster volume.

Four ordered setup steps for an Aurora PostgreSQL and Salesforce two-way sync: create a custom DB cluster parameter group and set rds.logical_replication, put the replication slot on the writer endpoint because readers cannot host one, upsert on a unique indexed External ID through Bulk API 2.0, and resume from a durable watermark because slot survival across failover is not guaranteed

If you have not picked a platform yet, the pillar on an enterprise iPaaS for Aurora PostgreSQL covers that decision, and two-way sync between Aurora PostgreSQL and NetSuite covers the ERP side of the same cluster. If you are on classic RDS rather than Aurora, the instance surface is different enough that syncing Amazon RDS with Salesforce is the better starting point.

Why Aurora and Salesforce end up disagreeing

They disagree because both are systems of record, for different halves of the same customer. Aurora owns the product: entitlements, usage counters, provisioning state, the rows an application writes on every request. Salesforce owns the commercial record: the account, the opportunity, the renewal date, the field a rep edits on a call. The trouble starts with the handful of values that are editable on both sides, which is almost always seat count, plan tier and status.

A one-way pipe hides the problem rather than solving it. A nightly load from Aurora into Salesforce works until a rep changes the seat count there and the application never hears about it. Someone then writes a second job going the other way, and now there are two jobs with no shared idea of which write happened last. That is where a job becomes a sync engine.

Aurora adds a wrinkle a single-instance PostgreSQL server does not have: the cluster is not one machine. Compute and storage are separate, and the volume is a distributed service shared by every instance, which is the design the Aurora origin story unpacks. For an integration that means several endpoints doing different jobs, and only one of them may hold your replication slot.

Setting up the Aurora side, in order

Five things, and the sequence matters because one of them needs a reboot of the writer. Create a custom DB cluster parameter group, set rds.logical_replication to 1, reboot the writer, create the role and publication, then create the slot. Everything after that is Salesforce work.

Here is where those steps sit in the path a committed row takes on its way to becoming a Salesforce record.

Five stage pipeline from Aurora PostgreSQL to Salesforce: a commit on the writer endpoint, decoding from the WAL once rds.logical_replication is on, mapping columns to fields with the primary key carried as the External ID, batched upserts through Bulk API 2.0, and a checkpoint that only advances the slot after Salesforce acknowledges the write
The slot advances last, which is what makes a restart safe.

Turn on logical decoding

rds.logical_replication is a DB cluster parameter, not an instance parameter, and it defaults to 0. Default parameter groups cannot be edited, so the first real step is creating a custom DB cluster parameter group and associating it with the cluster. Set the value to 1, reboot the writer instance, then confirm the result rather than assuming it. You never set wal_level yourself on Aurora, you set the flag and let the engine derive it.

Two things to budget for. Enabling the flag increases WAL generation even if you never create a slot, which shows up on the billed VolumeWriteIOPS metric. And since Aurora PostgreSQL 14.5, 13.8, 12.12 and 11.17, a write-through WAL cache reduces disk reads during logical decoding, on by default whenever logical replication is in use.

Put the slot on the writer, and only the writer

The replication slot lives on the publisher, and on an Aurora cluster the publisher is the writer instance. Readers cannot host one. AWS states it directly: PostgreSQL 16 added support for logical decoding from read replicas, and that feature is not supported on Aurora PostgreSQL. So the change-capture connection uses the cluster writer endpoint. The reader endpoint is wrong for a second reason too, since it balances connections rather than queries, randomly through DNS.

sql
-- run on the CLUSTER WRITER endpoint, after the reboot
SHOW wal_level;                     -- must return: logical

CREATE ROLE stacksync WITH LOGIN REPLICATION PASSWORD '...';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO stacksync;

CREATE PUBLICATION stacksync_pub
  FOR TABLE public.accounts, public.subscriptions, public.usage_daily;

-- the query that belongs on a dashboard from day one
SELECT slot_name, active, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;

Size the connections before you need to

Aurora derives the default connection ceiling from instance memory with LEAST({DBInstanceClassMemory/9531392}, 5000), capped at 5,000. AWS gives db.r4.large at 15.25 GiB as roughly 1,717 connections. Size the sync pool against that number rather than a guess. On Aurora Serverless v2 the value comes from the maximum ACU, so connections are not dropped when the cluster scales down.

RDS Proxy is worth considering, because it can cut failover time by up to 66% by routing to the new instance instead of waiting on DNS. It has one behaviour that catches integrations out: creating a temporary table pins a connection to its session and removes it from the pool, so a staging temp table in a batch job is the usual culprit. Prepared statements no longer force pinning on PostgreSQL targets.

Setting up the Salesforce side: External IDs, Bulk API 2.0 and CDC

Three pieces, in this order: an External ID field so writes are idempotent, Bulk API 2.0 for the backfill, and Change Data Capture for the return direction. The External ID is the one people skip and then regret, because without it every retry is a potential duplicate record.

An External ID is a custom field flagged as External ID, marked unique and indexed. Put the Aurora primary key in it, one per object you sync. Salesforce allows up to 25 per object, so the count is not the constraint, uniqueness is: an upsert that matches more than one record raises a DMLException rather than picking a winner. Populate the field before you upsert on it, and reconcile duplicates during that load.

For the initial load, Bulk API 2.0 batches for you rather than making you cut 10,000-record batches by hand the way v1 did. The ceilings that matter are 150 MB per job, 15,000 batches per rolling 24 hours shared across Bulk API and Bulk API 2.0, and 150,000,000 records per rolling 24 hours. A few million rows fits comfortably. A few hundred million needs planning across days.

Steady-state traffic comes out of a different bucket. The 24-hour API request allocation is shared across REST, SOAP, Bulk, Bulk 2.0 and Connect REST: 100,000 plus 1,000 per licence on Enterprise and Professional, 100,000 plus 5,000 per licence on Unlimited and Performance, a flat 15,000 on Developer, and 5,000,000 on a Full Sandbox. Sizing a sync against that number is worked through in the guide to Salesforce API limits in a Postgres sync.

Change Data Capture is where the return direction lives

For Salesforce to Aurora you want events, not polling. Change Data Capture publishes create, update, delete and undelete events for selected objects, and a consumer reads them with a stored replay ID. Two limits shape the design more than anything else here: events are retained on the event bus for 72 hours, and without the Change Data Capture add-on licence an org can enable CDC on a maximum of five objects.

The five-object cap is what decides your scope. Account, Contact, Opportunity and two custom objects, and you are done. Beyond that you either buy the add-on, which also adds 100,000 events per day, or fall back to a query-based sweep for the rest. The 72-hour window matters separately: an integration offline for longer than three days cannot resume from its replay ID and has to reconcile by querying instead. Delivery allocations before add-ons run from 10,000 events per rolling 24 hours on Developer to 50,000 on Unlimited, with a 1 MB maximum message size.

One round trip, and how the echo gets dropped

The mechanism that makes a two-way sync stable is origin tagging. Every write the engine makes carries the system it came from, so when that write reappears a moment later in the Aurora WAL or on the Salesforce event bus it is recognised as an echo and dropped rather than replayed.

Sequence diagram of one round trip: a WAL change leaves the Aurora writer through the replication slot, the engine tags the origin and stores the confirmed LSN, upserts through Bulk API 2.0 on the External ID, receives a Change Data Capture event back from Salesforce, stores the replay ID, updates the Aurora writer, then sees the same row return in the WAL and drops it as an echo
The echo at step six is the one that turns an unguarded two-way pipe into an infinite loop.

Two checkpoints carry the whole thing. On the Aurora side it is the confirmed LSN, and the slot advances past a change only once Salesforce has acknowledged the write. On the Salesforce side it is the replay ID. Both have to be durable, because both are what a restart resumes from, and on Aurora a restart is not a rare event.

When the same field moves on both sides inside the same window, something has to decide. Last writer wins is the common default and a poor one, because the outcome then depends on which job ran last. Field-level precedence is better, and it is mostly a conversation rather than an engineering problem: Aurora owns usage and provisioning state, Salesforce owns the commercial fields, and once every synced field has one named owner most collisions stop happening.

Book a Stacksync demo: keep AWS Aurora PostgreSQL and Salesforce in real-time two-way sync from the writer endpoint

Three ways to build it, and what each one costs you

There are three honest options: assemble it from AWS-native services, write it yourself, or run it on a managed two-way platform. The AWS-native route is the one most teams try first, and it is worth being precise about where it stops.

AWS DMS reads change data out of Aurora PostgreSQL well. It uses one of two logical decoding plugins, test_decoding or pglogical, preferring pglogical when it is installed on the source. The difference is where filtering happens: pglogical filters unwanted tables at the slot level, so less WAL, CPU and network, while test_decoding filters inside DMS and adds latency on long transactions. Enabling it means adding pglogical to shared_preload_libraries, restarting, then running CREATE EXTENSION pglogical.

What DMS does not do is write to Salesforce, and its bidirectional mode is not what the name suggests. DMS bidirectional replication is two independent one-way tasks with loopback prevention, and AWS states plainly that it "isn't intended as a full multi-master solution including a primary node, conflict resolution, and so on". Two more constraints apply to a PostgreSQL source: CDC requires primary keys, and there is no custom start time because PostgreSQL has no timestamp to LSN mapping. The AWS-native path is therefore DMS plus Amazon AppFlow or Lambda plus your own reconciliation, and the conflict policy is still yours.

AWS-native (DMS + AppFlow or Lambda)Custom codeStacksync
Aurora change captureDMS task on a slotYour own slot consumerManaged slot on the writer
Writes into SalesforceNot a DMS target, needs AppFlow or LambdaYour Bulk and REST clientBulk API 2.0 upsert on the External ID
Return directionA second, independent taskA second service to runThe same engine, both ways
Loop preventionLoopback filtering you configureYou build and test itOrigin tagging built in
Conflict policyNone, AWS says not multi-masterYou define and maintain itField-level precedence
Failover behaviourTask restart, resume from the slotYour checkpoint codeSlot re-established, replay from watermark
Salesforce quota controlYou meter it yourselfYou meter it yourselfBatched and metered by the engine
Schema changesEdit the task and reloadA code change and a deployRemapped in configuration
Realistic time to first syncWeeksWeeks to monthsHours

The same job, priced in engineering time rather than licence fees.

The custom-code column is not a strawman, and plenty of teams build it. The cost is that you then own a slot consumer with backpressure, durable checkpointing on both sides, Bulk job polling and error-file parsing, replay ID storage, echo suppression, idempotent retries and schema drift handling, forever. The Heroku Connect post-mortem is a good read on how that ages, and the platform comparison for PostgreSQL and Salesforce covers the vendor landscape.

Operating it: monitoring, failover, Blue/Green and upgrades

Four CloudWatch metrics and three cluster events. Watch OldestReplicationSlotLag, ReplicationSlotDiskUsage, FreeStorageSpace and TransactionLogsDiskUsage, and treat a stalled consumer as a database incident rather than an integration one.

The reason is that an inactive slot retains WAL. It blocks removal of old logs, which AWS notes can eventually lead to insufficient storage, and it blocks autovacuum from cleaning the catalog tables. A sync paused over a weekend is therefore not a backlog to clear on Monday, it is a cluster running low on volume. Alert on retained WAL size, not only on whether the process is up.

What a failover actually does

On failover the cluster endpoint keeps its DNS name and repoints to the promoted instance. Aurora DNS zones use a 5-second TTL, failover is typically restored in under 60 seconds and often under 30, and AWS names DNS propagation as the largest contributor, recommending client DNS TTL caching below 30 seconds. Your client library DNS cache settings are part of your recovery time whether you tuned them or not.

What is not documented is slot survival. AWS documents failover slots for classic RDS primary and read-replica topologies, not for Aurora clusters, so do not assume the slot comes through. Design for re-establishing it on the new writer and resuming from a durable watermark, which is only safe if every Salesforce write is idempotent. That is exactly what the External ID upsert buys you.

Blue/Green switchovers and major version upgrades

Two planned events will also take your slot away. A Blue/Green deployment needs a replication slot of its own for the green environment, and the documented AWS guidance is to drop your self-managed slots and subscriptions before the switchover and recreate them afterwards. A major version upgrade goes further: Aurora requires that all logical replication slots be dropped, including inactive ones, before it will proceed.

Neither is a problem if the sync resumes from a watermark and writes idempotently. Both are an outage if the design assumed a slot that lives forever.

To see this running without building the slot consumer yourself, look at the AWS Aurora PostgreSQL connector, the Aurora PostgreSQL and Salesforce integration, or book a demo. The same engine writes into Salesforce on the External ID and reads Change Data Capture back, so most of this guide becomes mapping and a precedence rule.

Start syncing AWS Aurora PostgreSQL and Salesforce in both directions with Stacksync

FAQ

Frequently asked questions

How do I sync AWS Aurora PostgreSQL with Salesforce in both directions?
Three pieces. On Aurora, create a custom DB cluster parameter group with rds.logical_replication set to 1, reboot the writer, then create a publication and a logical replication slot on the writer endpoint. On Salesforce, add a unique indexed External ID field and upsert into it with Bulk API 2.0 so retries do not duplicate. For the return direction, subscribe to Change Data Capture and write the events back to the writer. The engine in the middle tags the origin of every write so an echo is dropped rather than replayed.
How do I enable logical replication on Aurora PostgreSQL?
rds.logical_replication is a DB cluster parameter, not an instance parameter, and it defaults to 0. Default parameter groups cannot be edited, so create a custom DB cluster parameter group, associate it with the cluster, set rds.logical_replication to 1, and reboot the writer instance. After the reboot, SHOW wal_level returns logical. You never set wal_level directly. Note that enabling the flag increases WAL generation even before any slot exists, which shows up on the billed VolumeWriteIOPS metric.
Can an Aurora PostgreSQL read replica host a logical replication slot?
No. Replication slots live on the publisher, which on Aurora means the writer instance only. AWS states it plainly: PostgreSQL 16 added support for logical decoding from read replicas, and that feature is not supported on Aurora PostgreSQL. So point your change-capture connection at the cluster writer endpoint, not the reader endpoint. The reader endpoint also load-balances connections rather than queries, randomly by DNS, which makes it the wrong target for a long-lived streaming connection anyway.
What happens to my replication slot when Aurora fails over?
The cluster endpoint DNS name repoints to the new writer. Aurora DNS zones use a 5-second TTL and failover is typically restored in under 60 seconds, often under 30, with AWS naming DNS propagation as the largest contributor. Slot survival across an Aurora failover is not a documented guarantee, so do not design around it. Assume the slot has to be re-established on the new writer and that you resume from a durable watermark, which means every write into Salesforce has to be idempotent.
Can AWS DMS do two-way replication between Aurora PostgreSQL and another system?
Not in the way the phrase suggests. DMS bidirectional replication is two independent one-way tasks, A to B and B to A, with loopback prevention so a change does not bounce. AWS says explicitly that it is not intended as a full multi-master solution including a primary node, conflict resolution, and so on. There is also no conflict policy to configure. DMS CDC from a PostgreSQL source additionally requires primary keys on the tables and does not support a custom CDC start time, because PostgreSQL has no timestamp to LSN mapping.
How many Salesforce objects can I enable Change Data Capture on?
Five, unless you buy the Change Data Capture add-on licence, which removes the object cap and adds 100,000 events per day. Events sit on the event bus for 72 hours, so an integration that is down for longer than three days cannot replay from its stored replay ID and has to fall back to a query-based reconciliation pass. Event delivery per rolling 24 hours before add-ons is 50,000 on Performance and Unlimited, 25,000 on Enterprise and 10,000 on Developer, with a 1 MB maximum event message size.
What is the default max_connections on Aurora PostgreSQL?
Aurora derives it from instance memory with the formula LEAST({DBInstanceClassMemory/9531392}, 5000), so it scales with the instance class and is capped at 5,000. AWS gives db.r4.large with 15.25 GiB as roughly 1,717 connections. Size your sync connection pool against that number rather than guessing. On Aurora Serverless v2 the value is derived from the maximum ACU setting so connections are not dropped when the cluster scales down.
Will syncing Aurora with Salesforce use up my API request allocation?
It can, which is why the backfill and the steady state need different mechanisms. The 24-hour allocation is shared across REST, SOAP, Bulk API, Bulk API 2.0 and Connect REST: 100,000 plus 1,000 per licence on Enterprise and Professional, 100,000 plus 5,000 per licence on Unlimited and Performance, a flat 15,000 on Developer, and 5,000,000 on a Full Sandbox. Bulk API 2.0 keeps a backfill cheap, with 150 MB per job, 15,000 batches per rolling 24 hours and a 150,000,000 record ceiling.

About the author

Ruben Burdin
Ruben Burdin
Founder & CEO

Ruben Burdin is the Founder and CEO of Stacksync, the first real-time and two-way sync for enterprise data at scale. Ruben is a Y Combinator alumni with a strong background in software engineering and business.

All posts by Ruben Burdin

About Stacksync

Stacksync powers real-time, two-way sync between CRMs, ERPs, and databases. Engineers sync data at scale and automate workflows, not dirty API plumbing.

Coworkers laughing in front of a laptop in a casual office setting

Your last integration took months.
Your next one takes a prompt.