Bidirectional xCluster in YugabyteDB Anywhere: How to Build It Safely… and When Not To

Two independent YugabyteDB universes:

  • ● Both accept reads.
  • ● Both accept writes.
  • ● Changes made in either universe replicate asynchronously to the other.

That sounds like the perfect active-active architecture.

For some workloads, it can be.

For YSQL, however, bidirectional xCluster comes with consistency trade-offs that you need to understand before clicking Configure Replication.

In fact, YugabyteDB’s current documentation explicitly states that active-active multi-master replication is not recommended for YSQL deployments. The reason isn’t that replication itself doesn’t work. The challenge is preserving SQL consistency when two independent universes can modify related data concurrently.

⚠️ Start With the Architecture Question
If your application only needs one writable universe at a time, strongly consider transactional xCluster or xCluster DR instead.
Bidirectional xCluster is most appropriate when the application truly requires independent writes in both universes and the application can enforce rules that prevent unsafe concurrent modifications.

What Is Bidirectional xCluster?

xCluster is YugabyteDB’s asynchronous physical replication technology. Changes are captured from the DocDB WAL and replicated between two independent YugabyteDB universes.

The normal topology is unidirectional:

Universe A
Source
β†’
Universe B
Target

Bidirectional replication is really two independent unidirectional replication flows:

Universe A
Reads + Writes
A β†’ B
B β†’ A
Universe B
Reads + Writes

Changes received through xCluster are marked as external so that they aren’t simply replicated back to the universe they came from. That prevents an infinite replication loop.

Why Would I Want It?

The attractive use case is a geographically distributed application where users in two regions need local write latency.

For example:

🌎 EAST
Local application
Local reads
Local writes
🌎 WEST
Local application
Local reads
Local writes

Because replication between the universes is asynchronous, an EAST write doesn’t have to wait for WEST before returning to the application.

That’s the benefit.

The trade-off is eventual consistency between the two universes.

The Big YSQL Trade-Off: Bidirectional Means Non-Transactional

This is probably the most important technical fact in the Tip.

YugabyteDB’s transactional xCluster mode preserves transaction atomicity and consistent target reads, but the target is read-only.

Therefore:

  • Transactional xCluster cannot be bidirectional.

Bidirectional YSQL xCluster uses non-transactional replication.

βœ… Transactional xCluster
Transaction atomicity
Consistent target reads
Read-only target
Unidirectional only
⚠️ Bidirectional xCluster
Both universes accept writes
Asynchronous replication
Eventual consistency
Non-transactional

That difference is fundamental.

Convergence Is Not the Same Thing as SQL Correctness

This is where bidirectional xCluster gets interesting.

Two universes may eventually converge on the same physical state, while the resulting database state still isn’t what you would have gotten from a serial SQL execution.

Why?

Because writes from each universe are produced independently and arrive asynchronously.

That can affect:

  • ● predicate-based UPDATE and DELETE statements,
  • ● simultaneous changes to the same row,
  • ● secondary indexes,
  • ● unique constraints,
  • ● foreign keys,
  • ● triggers,
  • ● sequences,
  • ● and multi-row/multi-table transactions.

The safer deployment patterns are workloads where writes are explicitly separated… for example by table, partition, or non-overlapping primary/index key ownership.

What Is Relatively Safe?

The best bidirectional designs establish ownership.

Pattern 1: Different Tables

EAST writes only to EAST-owned tables.

WEST writes only to WEST-owned tables.

There should be no foreign-key or hidden trigger relationship crossing those ownership boundaries.

EAST Owns
east_orders
WEST Owns
west_orders

Pattern 2: Different Partitions

The same logical table can be used if writes are cleanly divided between regions.

EAST
Writes EAST partition
WEST
Writes WEST partition

Pattern 3: Different Keys

If both sides write the same table, another safer pattern is making sure they modify different primary-key, and, where applicable, different index-key ranges.

For example:

Universe EAST
Keys:
EAST-*
Universe WEST
Keys:
WEST-*

That is the pattern I’ll use in the demo.

When Should I Probably Not Use Bidirectional YSQL?

If you answer yes to several of these, I would seriously reconsider the architecture:

Requirement Concern
Same rows can be modified in both regions Last-writer-wins decides the surviving version.
Frequent predicate-based UPDATE or DELETE Rows not yet replicated into the executing universe can be missed.
Secondary or unique indexes are critical Unsafe concurrent modifications can leave index state inconsistent with the base table.
Cross-table foreign keys Constraints cannot be guaranteed across independent concurrent writes.
Trigger-generated writes Trigger side effects create hidden writes and replicated writes do not fire target-side triggers.
SERIAL / sequence-generated primary keys Sequence state is local to each universe and can generate conflicting values.
Strong transactional semantics required across regions Bidirectional xCluster is non-transactional.

The official documentation explicitly warns that concurrent bidirectional writes can violate unique constraints or leave indexes inconsistent.

Last-Writer-Wins Helps… But Only With One Kind of Conflict

Suppose both universes update the same primary key.

Universe EAST writes:

				
					UPDATE regional_notes
SET message = 'Updated in EAST'
WHERE note_id = 'conflict-001';
				
			

Universe WEST writes around the same time:

				
					UPDATE regional_notes
SET message = 'Updated in WEST'
WHERE note_id = 'conflict-001';
				
			

Bidirectional xCluster uses Last-Writer-Wins (LWW).

The update with the greater YugabyteDB Hybrid Logical Clock timestamp becomes the surviving value.

But LWW does not:

❌
Merge business logic
❌
Preserve predicate semantics
❌
Repair indexes
❌
Enforce cross-region constraints

And the word “last” refers to Hybrid Logical Clock order… not necessarily which operation a human observer thinks happened last.

πŸ”Ž See Last-Writer-Wins in Action
I explored this exact conflict in a previous YugabyteDB Tip, including an actual two-universe race and the HLC timestamps that determine the winner.

The Subtle Problem: A Statement Can Miss a Row

Last-Writer-Wins only helps when two writes actually conflict.

A different problem occurs when a row created in WEST has not yet replicated to EAST and EAST runs a predicate-based statement such as:

				
					DELETE FROM regional_notes
WHERE ...;
				
			

That statement can only affect rows EAST can currently see. If the new WEST row has not arrived yet, the DELETE never touches it. Later, the row replicates into EAST and still exists.

Step 1
WEST inserts a new row
↓
Step 2
Before that row reaches EAST, replication is still in flight.
↓
Step 3 β€” EAST Executes
DELETE FROM regional_notes WHERE …;
↓
Step 4
The statement can only operate on rows that EAST can currently see.
↓
Step 5
The new row from WEST has not arrived yet, so EAST’s DELETE never touches it.
↓
Step 6
Later, the WEST row arrives through replication.
↓
Result: Silently Missed DELETE
The row still exists because the statement ran before that row was visible in EAST.
The same risk applies to bulk UPDATE statements too.
⚠️ Last-Writer-Wins Cannot Fix a Write That Never Happened
If a row has not replicated yet, the remote UPDATE or DELETE cannot act on it. There is no conflicting write for Last-Writer-Wins to resolve.

DELETE + UPDATE: A Partial Row Can Appear

Another unsafe case is when one universe deletes a row while the other universe updates that same row before replication catches up.

Suppose both universes initially contain:

				
					INSERT INTO regional_notes
VALUES (
    'conflict-002',
    'EAST',
    'Original message',
    now()
);
				
			

Then, EAST deletes the row:

				
					DELETE FROM regional_notes
WHERE note_id = 'conflict-002';
				
			

At roughly the same time, WEST updates only part of that row:

				
					UPDATE regional_notes
SET message = 'Updated in WEST'
WHERE note_id = 'conflict-002';
				
			

Because those changes are generated independently and then replicated asynchronously, the converged result can be a partial row… the updated value survives, while other column values associated with the deleted row can be missing.

Step 1
The same row exists in EAST and WEST
↓
EAST
Deletes the row
WEST
Updates part of the row
↓
Step 3
The independent changes cross through asynchronous xCluster replication.
↓
Result: A Partial Row Can Remain
The updated value can survive while other values from the deleted row are missing.
⚠️ DELETE + UPDATE of the Same Row Is Not a Safe Pattern
This is another reason to avoid allowing both universes to concurrently modify the same logical rows in a bidirectional YSQL deployment.

Sequences Are Local Too

Sequence data isn’t replicated in non-transactional xCluster.

So this is a bad fit for an active-active primary key:

				
					id BIGSERIAL PRIMARY KEY
				
			

or an application-controlled key that includes regional ownership.

Schema Changes Are Another Reason to Think Twice

Transactional xCluster now has Automatic mode, which automatically replicates supported schema changes from the primary to the standby. Automatic mode is the recommended mode for new transactional deployments.

Bidirectional xCluster can’t use transactional mode because both sides accept writes.

That means DDL coordination becomes more operationally involved.

For example, with bidirectional xCluster, new indexes must be created concurrently on both universes for automatic index addition to replication. Creating the index on only one universe can time out. Some ALTER operations require replication to be paused on both sides.

⚠️ Need Simple Automatic DDL Replication?
This is another reason to ask whether bidirectional xCluster is really necessary.
If your application can use a single writable primary, transactional xCluster with Automatic mode provides a significantly simpler schema-management model.

Bidirectional xCluster or xCluster DR?

This is probably the decision table I’d show a customer first.

RequirementBetter Starting Point
One universe writes at a timexCluster DR / Transactional xCluster
Planned switchover and failoverxCluster DR
Automatic schema replicationTransactional xCluster Automatic mode
Both universes absolutely must accept independent writesConsider bidirectional xCluster… after reviewing the consistency model

xCluster DR provides transactional YSQL, read-only standby behavior, automatic DDL in current releases, and orchestrated switchover/failover.

Demo: Build Bidirectional xCluster with YugabyteDB Anywhere

Now that we’ve covered why I would not casually choose this topology, let’s build one.

For this lab I’m deliberately using a simple workload that follows a safer pattern:

  • EAST and WEST will write different primary-key values.

There are no secondary indexes, foreign keys, or sequences.

Demo Environment

I’m running YugabyteDB:

				
					yugabyte=# SELECT split_part(version(), '-', 3) yb_version;
 yb_version
------------
 2026.1.2.0
(1 row)
				
			

I’ll use two universes:

EAST
Universe A
WEST
Universe B

For an empty-table lab, YugabyteDB’s non-transactional setup procedure expects the same schema on both universes. If you have existing data instead, use the appropriate bootstrap process.

Create the Demo Database

On EAST:

				
					yugabyte=# CREATE DATABASE xcluster_demo;
CREATE DATABASE

yugabyte=# \c xcluster_demo
You are now connected to database "xcluster_demo" as user "yugabyte".

xcluster_demo=# CREATE TABLE regional_notes (
xcluster_demo(#     note_id     text PRIMARY KEY,
xcluster_demo(#     origin      text NOT NULL,
xcluster_demo(#     message     text NOT NULL,
xcluster_demo(#     updated_at  timestamptz NOT NULL DEFAULT now()
xcluster_demo(# );
CREATE TABLE
				
			

For this lab, create the same empty database and table on WEST as well.

				
					CREATE DATABASE xcluster_demo;

\c xcluster_demo

CREATE TABLE regional_notes (
    note_id     text PRIMARY KEY,
    origin      text NOT NULL,
    message     text NOT NULL,
    updated_at  timestamptz NOT NULL DEFAULT now()
);
				
			
πŸ’‘ Notice What I’m NOT Adding
No secondary index.
No foreign key.
No sequence-generated primary key.
That’s intentional. I want the first replication demo to use a simple, predictable workload before introducing a conflict later in the demo.

Step 1: Configure EAST β†’ WEST in YBA

In YugabyteDB Anywhere, navigate to the EAST universe and open the xCluster Replication tab.

Choose Configure Replication.

Use something like:

Replication Name
east-to-west
Source Universe
EAST
Target Universe
WEST
Database
xcluster_demo
Replication Type
Non-Transactional

Do not enable transactional atomicity; a bidirectional configuration requires non-transactional replication.

YBA will validate the database selection and, if bootstrap is required because existing data needs to be copied, walk you through the backup/restore process. Note that a full-copy setup can replace target tables, so don’t point this at target data you care about without understanding the bootstrap operation.

Here’s what the setup looks like:

After setup completes:

Step 2: Configure WEST β†’ EAST

Now reverse the roles.

From the WEST universe, create another xCluster configuration:

Replication Name
west-to-east
Source Universe
WEST
Target Universe
EAST
Database
xcluster_demo
Replication Type
Non-Transactional

The set up is the same, only in reverse:

After setup completes:

Bidirectional xCluster is now:

EAST
Reads + Writes
east-to-west β†’
← west-to-east
WEST
Reads + Writes

Test 1: EAST Owns EAST Keys

Connect to EAST:

				
					INSERT INTO regional_notes
VALUES (
    'EAST-001',
    'EAST',
    'Created locally in EAST',
    now()
);
				
			

On WEST:

				
					SELECT *
FROM regional_notes
ORDER BY note_id;
				
			

We should see EAST-001.

Test 2: WEST Owns WEST Keys

Now insert on WEST:

				
					INSERT INTO regional_notes
VALUES (
    'WEST-001',
    'WEST',
    'Created locally in WEST',
    now()
);
				
			

Back on EAST:

				
					SELECT *
FROM regional_notes
ORDER BY note_id;
				
			

After replication catches up, both universes should contain:

				
					EAST-001
WEST-001
				
			

This demonstrates each direction independently, which is also the recommended way to validate bidirectional replication.

βœ… This Is the Pattern We Want
EAST created an EAST-owned primary key.
WEST created a WEST-owned primary key.
Both universes can accept writes, but they are not competing to modify the same logical data.

Bonus Demo 1: Intentionally Break the Ownership Rule

Now let’s deliberately do something we normally want the application to avoid.

Create a common row and let it replicate:

				
					INSERT INTO regional_notes
VALUES (
    'conflict-001',
    'EAST',
    'Starting value',
    now()
);
				
			

Confirm it exists on both sides.

Then, around the same time:

On EAST:

				
					UPDATE regional_notes
SET origin = 'EAST',
    message = 'Updated independently in EAST',
    updated_at = clock_timestamp()
WHERE note_id = 'conflict-001';
				
			

On West:

				
					UPDATE regional_notes
SET origin = 'WEST',
    message = 'Updated independently in WEST',
    updated_at = clock_timestamp()
WHERE note_id = 'conflict-001';
				
			

After replication settles:

				
					SELECT *
FROM regional_notes
WHERE note_id = 'conflict-001';
				
			

Both universes should eventually converge on one winner.

But remember: the updated_at column does not decide the winner.

The winner is determined by YugabyteDB’s internal hybrid timestamp.

πŸ§ͺ This Demo Proves the Conflict Mechanism… Not That Conflicts Are Desirable
We’re intentionally violating our regional ownership rule to show how Last-Writer-Wins behaves.
A production design should minimize the opportunity for this conflict to happen at all.

Bonus Demo 2: Add a Secondary Index

Earlier, I intentionally created regional_notes without a secondary index.

Now that bidirectional replication is healthy, let’s add one.

The important part is that for a YSQL table participating in bidirectional xCluster, the index should be created concurrently on both universes.

Open two ysqlsh sessions, one connected to EAST and one connected to WEST.

On EAST

				
					CREATE INDEX regional_notes_origin_idx
ON regional_notes(origin);
				
			

At Approximately the Same Time, on WEST

				
					CREATE INDEX regional_notes_origin_idx
ON regional_notes(origin);
				
			

YugabyteDB should detect that the indexed table is already participating in bidirectional xCluster and automatically add the new indexes to replication in both directions. The underlying capability is enabled by default through ysql_auto_add_new_index_to_bidirectional_xcluster.

After both commands finish, verify the schema on each universe:

				
					\d regional_notes
				
			

You should see the new index:

				
					"regional_notes_origin_idx" lsm (origin ASC)
				
			

Verify It in YBA

Return to the xCluster replication views in YugabyteDB Anywhere and confirm that both replication configurations remain healthy.

We can also see the index has been added to the replication:

⚠️ Create the Index on Both Universes
For a YSQL table in bidirectional xCluster, issue the corresponding CREATE INDEX on both universes at roughly the same time.
Creating the index on only one universe can cause the operation to time out and fail.

But Don’t Miss the Bigger Point

Successfully adding the index does not make unsafe concurrent write patterns safe.

If EAST and WEST independently modify the same primary-key or index-key data, non-transactional bidirectional replication can still leave the base table and secondary index inconsistent. YugabyteDB specifically warns about conflicting writes involving indexes and unique constraints.

πŸ’‘ DDL Success β‰  Safe Concurrent DML
This demo proves that a new secondary index can be added to a bidirectionally replicated YSQL table.
It does not change the consistency rules discussed earlier. Continue to avoid concurrent writes that compete for the same primary-key or index-key data.

Want to Automate the Setup?

Once you’ve learned the setup through the YBA interface, xCluster can also be managed through the YBA CLI.

πŸ€– Automate It with YBA CLI
The UI is useful for understanding the workflow and taking screenshots. For repeatable deployments, CI/CD pipelines, and scripted environments, the same xCluster operations can be automated using the YBA CLI.

One More Operational Consideration: Upgrades

Bidirectional xCluster makes software upgrades more nuanced because each universe is both a source and a target.

With unidirectional xCluster, the normal rule is straightforward: upgrade and finalize the target first, then the source. The target must not end up older than the source.

With bidirectional xCluster, that simple ordering no longer works because both universes are simultaneously sending and receiving replicated changes.

The current guidance is:

1. Upgrade Universe B
↓
2. Upgrade Universe A
↓
3. Monitor and Validate Both Universes
Check replication health and run application-level validation.
↓
4. Finalize Both Upgrades
Finalize A and B as close together as possible.

During the Monitor phase, validate both universes before finalizing. YugabyteDB specifically recommends application-level testing as needed, and notes that features relying on PITR are not available during this phase.

If you discover a problem before finalization, the guidance is to roll back both universes, not just one.

The most important detail is the finalization step. If both sides are actively taking writes, they should be finalized as close together as possible. Otherwise, replication in the direction from the newer finalized version toward the older universe can pause.

⚠️ Don’t Treat the Two Upgrades Independently
In bidirectional xCluster, both universes are simultaneously a replication source and target.
Upgrade both, validate both, and keep the time between finalizing the two universes as short as possible.

One useful exception: if writes are intentionally happening on only one side, YugabyteDB documents that you can finalize the non-writing universe first and then proceed with the universe accepting writes.

Final Takeaway

Bidirectional xCluster is powerful.

It gives two independent YugabyteDB universes the ability to serve local reads and local writes while asynchronously exchanging changes.

But for YSQL, the difficult part isn’t creating the two replication streams.

The difficult part is designing an application that remains correct while both sides are writing.

πŸ’‘ The Key Point
⇄ Two Writable Universes
Bidirectional xCluster allows low-latency local writes in both locations.
🧭 Establish Ownership
Divide writes by table, partition, or key whenever possible so both universes do not compete for the same data.
⚠️ Know the Consistency Model
Last-Writer-Wins is not a substitute for transaction atomicity, index consistency, constraints, or application-level conflict handling.
If your workload does not truly require both universes to accept writes, transactional xCluster or xCluster DR is usually the simpler and safer YSQL architecture.

Have Fun!

While visiting D.C. to meet with a customer, my wife, who tagged along for the ride, and I made time to visit the Smithsonian’s National Zoo. We had the privilege of seeing baby elephant Linh Mai, and she was absolutely adorable! 🐘❀️

It was hard to tell who Mom was because all the adults seemed eager to watch over the little one. Apparently, babysitting is a team sport in the elephant world! 😊