Home Posts PostgreSQL to Serverless SQL Migration [2026 Guide]
Cloud Infrastructure

PostgreSQL to Serverless SQL Migration [2026 Guide]

PostgreSQL to Serverless SQL Migration [2026 Guide]
Dillip Chowdary
Dillip Chowdary
Tech Entrepreneur & Innovator · April 26, 2026 · 10 min read

Bottom Line

The safest migration shape is backfill first, stream changes second, and switch traffic only after verification is clean. Your application can stay online for almost the entire move, but you should still plan for a short write-drain at cutover.

Key Takeaways

  • Use molt fetch plus replicator pglogical; avoid the legacy combined replication modes.
  • Set wal_level=logical and size max_replication_slots/max_wal_senders before you start.
  • Default IMPORT INTO is faster; use --use-copy only if target tables must stay queryable during load.
  • Cut over only when replication activity is effectively zero and molt verify shows no missing or mismatched rows.

A clean PostgreSQL exit is not a dump-and-pray exercise. The production-safe pattern is: convert the schema, bulk-load historical rows, stream live changes, verify, then cut over only when replication has drained. This walkthrough uses CockroachDB Cloud Basic as the concrete destination because Cockroach Labs publishes an official PostgreSQL-to-distributed-SQL migration flow built around MOLT Fetch, MOLT Replicator, and MOLT Verify.

  • Use separate tools for each phase: molt fetch for the backfill, replicator pglogical for live change capture, and molt verify for data checks.
  • Prepare PostgreSQL correctly: logical replication needs wal_level=logical, plus enough replication slots and WAL senders for the stream.
  • Treat schema drift as a release blocker: DDL during replication is risky, and PostgreSQL-only features still need review on the target.
  • Make cutover boring: pause or queue writes, wait for the apply rate to hit zero, verify, then switch the application DSN.
DimensionPostgreSQLServerless distributed SQLEdge
Operational modelSimple single-cluster opsManaged autoscaling and multi-node distributionDistributed SQL
Write scalabilityPrimarily verticalHorizontal by designDistributed SQL
Behavioral compatibilityNative baselineHigh, but not perfectPostgreSQL
Backfill speedN/AIMPORT INTO is optimized for bulk loadDistributed SQL
Migration riskNo move requiredNeeds schema audit, CDC, and disciplined cutoverPostgreSQL

Plan the Migration

Prerequisites

  • A PostgreSQL primary you can reconfigure and restart if wal_level is not already logical.
  • A target CockroachDB Cloud cluster and a database ready to receive converted DDL.
  • MOLT binaries installed on a host that can reach both databases and your object store.
  • An object storage bucket for the initial backfill, such as s3://..., gs://..., or Azure Blob Storage.
  • A rollback window and a change freeze for schema changes during the live replication phase.
  • A masked rehearsal dataset for at least one full dry run. If your clone contains customer data, sanitize it first with TechBytes' Data Masking Tool.

Bottom Line

Do not chase a single big-bang cutover. Backfill first, let CDC absorb ongoing writes, and reserve the final switch for a short, controlled write drain.

The current Cockroach Labs guidance favors a separate workflow instead of the older all-in-one replication mode. That matters because it keeps the migration easier to observe: you get a checkpoint from the backfill, then explicitly start continuous replication from that point. Architecturally, this is the right shape even if you later swap in a different distributed SQL vendor.

Watch out: Replication moves rows, not behavioral equivalence. Audit PostgreSQL-only features and edge-case SQL semantics before the migration window, especially anything tied to unsupported types, extensions, or query behavior differences.

Step 1: Prepare Source and Target

Prepare PostgreSQL for logical replication

First, confirm that you are talking to the primary. Cockroach's replication tooling requires the source primary, and PostgreSQL's logical replication settings are publisher-side concerns.

SELECT pg_is_in_recovery();
ALTER SYSTEM SET wal_level = 'logical';

If pg_is_in_recovery() returns t, you are on a replica and should stop. Also note that PostgreSQL documents wal_level as a startup-only parameter, so changing it requires a restart. On the publisher, PostgreSQL also requires maxreplicationslots to cover your expected subscriptions plus reserve, and maxwalsenders to be at least that high plus any physical replicas.

Create a dedicated migration user. The exact privilege model depends on how tightly you want to scope ownership, but the pattern below is the minimal baseline for data reads:

CREATE USER migration_user WITH PASSWORD 'replace-me';
GRANT CONNECT ON DATABASE appdb TO migration_user;
GRANT USAGE ON SCHEMA public TO migration_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO migration_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO migration_user;
ALTER USER migration_user WITH LOGIN REPLICATION;
GRANT CREATE ON DATABASE appdb TO migration_user;

If you want MOLT to manage publications without temporary superuser access, the replicated tables also need to be owned by that user.

Prepare the distributed SQL target

Load a converted schema into the target before moving rows. CockroachDB is PostgreSQL-wire compatible, but Cockroach's own compatibility guide is explicit that some PostgreSQL features and behaviors differ. For faster initial loads, Cockroach recommends dropping non-primary-key constraints and secondary indexes first, then recreating them after data is synchronized.

CREATE DATABASE appdb;
CREATE SCHEMA IF NOT EXISTS public;

Keep this rule simple: source and target schema definitions must match the rows you plan to replicate. If your source uses PostgreSQL-specific constructs, fix them now instead of during the cutover.

Step 2: Run the Initial Load

The backfill seeds the target with a consistent snapshot and creates the replication checkpoint you need for live sync. In the PostgreSQL flow, MOLT can also create the publication and slot automatically if you pass both required pglogical flags.

export SOURCE='postgresql://migration_user:REDACTED@pg-primary.example.com:5432/appdb?sslmode=require'
export TARGET='postgresql://roach_user:REDACTED@cockroach.example.com:26257/appdb?sslmode=verify-full'

molt fetch --source "$SOURCE" --target "$TARGET" --schema-filter 'public' --table-filter 'customers|orders|payments' --bucket-path 's3://my-migration-bucket/appdb' --table-handling truncate-if-exists --pglogical-replication-slot-name molt_slot --pglogical-publication-and-slot-drop-and-recreate

This is the point where teams make their most expensive mistake: they optimize for purity instead of observability. Keep the table filter tight for the first production move. If you need the target to stay queryable during the load, Cockroach documents --use-copy as the online option, but the default IMPORT INTO path is faster for large backfills.

Pro tip: Capture the final cdc_cursor from the fetch logs and store it with the change ticket. That checkpoint is your evidence that the live replicator started from a known position.

Expected output

message=starting fetch
message=dropping and recreating publication molt_fetch
message=fetch complete cdc_cursor=0/3F41E40

If you do not see a completion record with a checkpoint, do not proceed to CDC.

Step 3: Start CDC and Cut Over

Once the backfill is complete, start the live replicator against the same source and target. For PostgreSQL, Cockroach's documented flow uses replicator pglogical with the slot name created during fetch and the publication name molt_fetch.

replicator pglogical --sourceConn "$SOURCE" --targetConn "$TARGET" --targetSchema appdb.public --slotName molt_slot --publicationName molt_fetch --stagingSchema appdb._replicator --stagingCreateSchema --metricsAddr :30005 -v

Leave that process running while production still writes to PostgreSQL. During this phase, the application remains online and the target catches up in the background. What you are waiting for is not elapsed time; it is confidence.

Cutover checklist

  1. Freeze DDL. Cockroach documents that schema changes while replication is in progress can break the pipeline.
  2. Pause or queue source writes. This is the brief drain window that turns a risky cutover into a predictable one.
  3. Wait until replication has drained. Cockroach documents two signals: no new upserted rows logs, or a combined apply rate of zero from the metrics endpoint.
  4. Run final verification.
  5. Switch application connection strings to the new cluster.
  6. Rebuild the secondary indexes and non-primary-key constraints you deferred for load speed.
sum by (schema) (rate(apply_upserts_total[$__rate_interval]) + rate(apply_deletes_total[$__rate_interval]))

When that expression is effectively zero and your application write queue is empty, stop the replicator cleanly and flip traffic.

Verify and Troubleshoot

Verification and expected output

Run molt verify before and after the final write drain. The first pass tells you whether the bulk load is sane. The second pass proves the live sync closed the gap.

molt verify --source "$SOURCE" --target "$TARGET" --table-filter 'customers|orders|payments'
message=starting verify on public.orders, shard 1/1
message=finished row verification on public.orders (shard 1/1) num_missing=0 num_mismatch=0 num_extraneous=0

If you want a fast human spot-check, compare row counts for your highest-churn tables on both sides before the DSN switch. Verification is your go/no-go gate; row counts are just a sanity layer.

Top 3 migration failures

  1. Replication never catches up: verify you are connected to the PostgreSQL primary, confirm wal_level=logical, and make sure slot and sender capacity are sized for the stream.
  2. Replication breaks after a schema change: stop making DDL changes during CDC. The safest move is a release freeze from fetch start until post-cutover validation finishes.
  3. Rows still differ after cutover: check for operations that the documented flow does not capture cleanly in your process, especially source-side TRUNCATE, and confirm the target schema still matches the source.

When to Choose Each and What's Next

Choose PostgreSQL when:

  • You need exact PostgreSQL behavior and rely on extensions or features your distributed target does not fully match.
  • Your write scale still fits a vertically scaled primary plus replicas.
  • Your team is not ready to absorb new transaction, topology, and retry semantics yet.

Choose serverless distributed SQL when:

  • You need horizontal write scaling, multi-region survivability, or simpler managed operations.
  • Your workload is already application-led and you can tolerate a disciplined migration project.
  • You want a PostgreSQL-compatible developer surface but cannot keep solving growth with bigger single nodes.

What's next

Frequently Asked Questions

Can I migrate PostgreSQL to distributed SQL with truly zero downtime? +
Not automatically. The documented MOLT flow keeps the application live for most of the move, but Cockroach Labs still recommends briefly stopping or queueing source writes so replication can drain before cutover. If you need absolutely no write pause, you need application-level buffering or dual-write controls.
Should I use IMPORT INTO or --use-copy for the initial load? +
Use the default IMPORT INTO path when load speed matters most; Cockroach documents it as the fastest option for large backfills. Use --use-copy only when the target tables must remain online and queryable during the load, because that mode trades throughput for availability.
How much PostgreSQL compatibility work should I expect on CockroachDB? +
Less than a full rewrite, but not zero. CockroachDB supports the PostgreSQL wire protocol and much of the syntax, yet its compatibility docs still call out unsupported features and behavioral differences. Audit extensions, types, and edge-case SQL before the migration window.
Why do I still need molt verify if replication is already running? +
Because replication health is not proof of row-level correctness. molt verify checks table structure and row values, and its summary output makes the final go/no-go decision defensible. Treat clean verification as mandatory before you switch production traffic.

Get Engineering Deep-Dives in Your Inbox

Weekly breakdowns of architecture, security, and developer tooling — no fluff.

Found this useful? Share it.