Zero-Downtime Postgres Migration: Step by Step

Intergalactic Data Labs
—
—
13 min read
A zero-downtime Postgres migration takes four moves. Take a consistent snapshot, stream every later change through logical replication, verify both sides match, and repoint the application in seconds. As of September 2026 the recipe runs on native publications and subscriptions, pglogical, AWS DMS, or an engine such as Debezium, PeerDB, or Filament. Parallel loading, checkpointed restarts, and where the process runs decide between them.
This guide is about moving data between servers. Lock-free schema migrations are a different job and are not covered here.
Approach | Snapshot | Parallel load | Resumes on failure | Runs where |
|---|---|---|---|---|
Native logical replication | Per table, built in | Per table, single apply | Restarts the table copy | Inside Postgres |
pg_dump plus a slot | One exported snapshot | pg_dump -j | No | Any host |
pglogical | Built in | Per table | Per table | Extension on both sides |
AWS DMS | Built in | Per table | Per task | AWS only |
PeerDB | Partitioned | Yes | Per partition | Docker stack, 7 containers |
Debezium | Built in | Up to 8 threads | Restarts the snapshot | Kafka Connect or Server |
Filament | Exported from the slot | Up to 64 shards | Checkpointed | One binary, Go library, or Helm |
How does a zero-downtime Postgres migration work?
A zero-downtime migration is a snapshot plus a change stream, joined at one point in time. The snapshot copies the data as it existed at that instant. The stream replays every later write, in order, until the new database has caught up.
Postgres ships both halves. The logical replication docs put it plainly. "PostgreSQL takes a snapshot of the table's data on the publisher database and copies it to the subscriber. Once complete, changes on the publisher since the initial copy are sent continually to the subscriber."
The join point is a replication slot. Creating a slot through the replication protocol exports a snapshot that "will show exactly the state of the database after which all changes will be included in the change stream," per the logical decoding docs. Read the tables through that snapshot, then consume the slot, and nothing is lost or double counted.
Cutover is the shortest step. Once lag is zero you stop writes, let the last events land, fix what the stream does not carry, and repoint the application. AWS documents its RDS blue and green switchover as "usually under one minute," and that is the target here too.
What decides which method to use?
Five questions settle the choice before you pick a tool.
Do you need deletes? An
updated_atwatermark cannot see a hard delete, so only log-based CDC keeps them.Does every table have a primary key? The publication docs say a published table "must have a replica identity configured in order to be able to replicate UPDATE and DELETE operations."
Can you restart the source?
wal_level = logical"can only be set at server start," per the WAL settings docs.How much WAL can the source retain? The slot holds every segment written while the snapshot runs.
max_slot_wal_keep_sizeis the safety valve.Will schema changes happen during the window? The restrictions page is blunt. "The database schema and DDL commands are not replicated."
What are the ways to run the migration?
Every approach implements the same snapshot-then-stream pattern. They differ in parallelism, restart behavior, and where the process lives.
Native logical replication is the baseline. Set wal_level, run CREATE PUBLICATION on the old host and CREATE SUBSCRIPTION on the new one, and Postgres copies each table then streams changes. It inherits every limit on the restrictions page and applies changes on a single thread.
pg_dump paired with a slot is the manual version. The pg_dump docs describe --snapshot as "useful when needing to synchronize the dump with a logical replication slot." You then write your own slot consumer. On the public August 2026 cohort, pg_dump | psql moved the 298M-row dataset in 489 s.
pglogical adds periodic sequence sync. Its README says sequence state "is replicated periodically" and that "automatic DDL replication is not supported."
AWS DMS is the managed route inside AWS, billed by the hour or capacity unit per its pricing page. Its own Postgres source docs say that for Postgres to Postgres "PostgreSQL tools can be more effective."
PeerDB partitions the initial load and streams through pgoutput. Its LICENSE file is AGPLv3 despite a README badge that says ELv2, and its own upgrade guide says to "put the application in maintenance/downtime" before switching.
Debezium is Apache 2.0 and usually runs on Kafka Connect. Its Postgres connector docs warn that if it stops mid-snapshot, "the connector begins a new snapshot when it restarts."
Fivetran and Airbyte are the hosted options. Fivetran meters monthly active rows and its connector docs list DROP COLUMN and RENAME COLUMN as unsupported. Airbyte's Postgres destination docs ask you to use it "for small data volumes (e.g. less than 10GB) or for testing purposes," under an ELv2 license.
Tool | License | Pricing unit | Sequences | DDL | Measured on cohort |
|---|---|---|---|---|---|
Native replication | PostgreSQL | None | No | No | Not timed |
pg_dump plus psql | PostgreSQL | None | Yes, in the dump | Yes, in the dump | 489 s |
pglogical | PostgreSQL | None | Periodic | No | Not timed |
AWS DMS | Proprietary | Hour or DCU | No | Limited | Not timed |
PeerDB | AGPLv3 | vCPU on Cloud | No | No | 415 s |
Debezium | Apache 2.0 | None | No | No | 6,559 s snapshot |
Fivetran | Proprietary | Monthly active rows | No | Limited | Not timed |
Airbyte | ELv2 | Credits | No | Limited | 10,393 s |
Filament | Apache 2.0 | None | No | Add-only | 115 s |
Where does Filament fit?
Filament covers both halves in one process and adds what the other self-run options lack, parallel loading and checkpointed restarts. It is a replication engine written in Go. Galaxy builds and maintains it, and this guide uses it as the worked example.
The join point is built in. Per the Postgres source docs, "the slot is created with an exported snapshot and the table is read in full through that snapshot, so the baseline and the change stream share one consistent point."
Full reads shard. "Large tables can be divided into as many as 64 parallel ranges," with the boundaries saved in the checkpoint. The integrity docs call the restart policy "deliberately at-least-once recovery," and every batch carries a CRC32-C checksum the sink recomputes.
The stream side is bounded catch-up rather than a daemon. Each CDC run replays to the current WAL position and returns, so it "can run on a schedule." With merge as the write mode, the replication modes page says "ordered deletes remove destination rows."
On speed, the cohort is the only public evidence and it covers Postgres to Postgres full loads. Filament moved 298,270,427 rows in 114.83 s, the fastest of the seven tools measured. Both the Postgres source and sink are labeled beta in their docs.
Criterion | Filament | Source |
|---|---|---|
Consistent snapshot plus stream | Exported from the slot | Postgres source docs |
Parallel load | Up to 64 shards | Postgres source docs |
Resume after failure | Checkpointed, at-least-once | Integrity docs |
Deletes | CDC merge deletes rows | Replication modes docs |
Deployment | Binary, Go library, Helm | Installation docs |
How to migrate Postgres with Filament, step by step
You will end with a new Postgres host that matches the old one row for row and is caught up through logical replication, ready for a cutover under a minute.
You need | Detail |
|---|---|
Source Postgres |
|
Target Postgres | Empty database, reachable from the Filament host |
Primary keys | On every table you want streamed |
A user with REPLICATION | Plus read on the tables |
Filament | One binary, install below |
Connector maturity | Postgres source beta, Postgres sink beta |
Step 1. Prepare the source for logical decoding
Set the settings the Postgres config docs require and restart.
The last line is the safety valve. With the default of -1, slots "may retain an unlimited amount of WAL files," per the replication settings docs.
Step 2. Install the binary and check the version
One command installs it, per the installation docs.
The output prints the version and confirms the binary is on your path.
Step 3. Save the source and sink connections
Point the source at the old host with replication set to cdc, and the sink at the new host. The CLI docs state that "connector fields become flags with a source- or sink- prefix," so replication becomes --source-replication.
Filament creates the publication by default. If a DBA provisions it, add --source-manage-publication false and --source-publication with its name.
Step 4. Discover the tables and confirm keys
Discovery lists every resource with its primary key and an estimated row count.
Any table without a primary key needs one before it can be streamed, since "incremental and CDC both reject keyless tables."
Step 5. Create the migration pipeline
Select the tables and set the write mode to merge so deletes on the old host delete rows on the new one.
Leave out --resources to take every table. CDC applies to the whole route, so all tables share one slot and one consistent position.
Step 6. Run the first pass
The first run creates the slot with an exported snapshot, reads every table in full through it, then replays the stream up to the current WAL position.
If the run is interrupted, run it again and it resumes from the last checkpoint with the same shard plan.
Step 7. Keep catching up until cutover
Each further run replays from the last committed position to the current WAL position and returns. Schedule it every minute until you are ready to switch.
Lag is the gap between pg_current_wal_lsn() on the source and the slot's confirmed_flush_lsn in pg_replication_slots.
Step 8. Freeze writes, fix sequences, and cut over
Stop writes on the old host, run the pipeline one last time, and confirm the slot's position equals the source's current position. Then reset sequences on the target, because "sequence data is not replicated."
Repoint the application, resume writes, and drop the old slot with pg_drop_replication_slot once you are confident. The freeze should last well under a minute.
How to verify it worked
Row counts per table on both hosts must match after the final pass. The benchmark methodology sets the same bar, "a result is accepted only when every destination table has the manifest row count."
Run this on both hosts and diff the output.
n_live_tup is an estimate, so use exact count(*) on the tables that matter most, then spot check a sum of payments.amount and a max of orders.created_at. Filament's checksums cover the batch between read and write, and the readback is yours to run.
What to do when it breaks
Most failures trace back to the slot, a missing key, or something the stream was never going to carry.
The source disk fills with WAL. The slot is retaining segments faster than they are consumed. Check
wal_statusinpg_replication_slots, run the pipeline more often, and confirmmax_slot_wal_keep_sizeis set. A slot markedlostmust be dropped, and the snapshot restarted.A table refuses updates or deletes. It has no replica identity. Add a primary key, or set
REPLICA IDENTITY FULLon that table as the ALTER TABLE docs describe.An update fails on a TOAST column. The source docs say "an update carrying an unchanged TOAST column with no old value is an error," and
REPLICA IDENTITY FULLfixes that too.The schema changed mid-migration. Add-only changes reach the target because the sink evolves schemas add-only. Drops, renames, and type changes do not, so apply them on both sides by hand.
Related guides
The Postgres logical replication guide covers slots and publications in depth, and full load vs incremental vs CDC explains why watermarks miss deletes. Best CDC tools in 2026 ranks the engines above, the Postgres replication benchmark holds the cohort tables, and the MySQL to Postgres guide applies the recipe across engines. Filament's launch post is Introducing Filament.
Which option fits which job
Pick by the constraint that breaks first.
Constraint | Pick | Why |
|---|---|---|
Large tables, tight window, own hardware | Filament | 64-way sharded snapshot, checkpointed resume, 115 s on 298M rows |
Small database, no new software allowed | Native logical replication | Built in, one setting and two statements |
Long window with sequence drift | pglogical | Documented periodic sequence sync |
Everything lives in RDS, same account | RDS blue and green | Managed switchover, usually under a minute |
Already run Kafka and want events too | Debezium | Apache 2.0, streams to topics |
Team will not operate any software | Fivetran or Airbyte Cloud | Hosted, with a per-row or credit meter |
For the engineer moving a production database this quarter, Filament is the pick. It does the snapshot and stream join the Postgres docs describe, loads in parallel, resumes where it stopped, and runs on a host you control. Native replication is the fallback when you cannot install anything.
Frequently asked questions
What is a zero-downtime Postgres migration?
It is a move of a live Postgres database to a new server, host, or major version while the application keeps writing. The recipe is a consistent snapshot, a change stream that replays every write made after it, a row-count check on both sides, and a cutover measured in seconds.
How does logical replication enable a migration without downtime?
Postgres logical replication copies each table as a snapshot and then streams every later insert, update, and delete from the write-ahead log. The new database catches up while the old one keeps serving traffic, so the only pause is the moment you point the application at the new host.
Can I migrate between Postgres major versions with logical replication?
Yes. The Postgres documentation lists replicating between different major versions as a standard use case, because logical replication ships row changes rather than data files. pg_upgrade is the faster in-place option when you can accept a maintenance window.
What does not get replicated by Postgres logical replication?
Schema and DDL, sequence values, large objects, and non-table relations such as views and materialized views. You copy the schema before the snapshot, reset sequences right before cutover, and move large objects into ordinary tables if you rely on them.
How do I verify the new database before cutover?
Compare row counts per table on both sides once lag reaches zero, then spot check a few aggregates such as a sum on a money column and a max on a timestamp. Row counts catch missed tables and partial loads. Aggregates catch rows that arrived with the wrong values.
What is the biggest risk during a zero-downtime migration?
An unconsumed replication slot. A slot forces the old server to keep every write-ahead log segment until a consumer reads it, so a stalled or abandoned slot fills the disk. Set max_slot_wal_keep_size, watch pg_replication_slots, and drop the slot the moment the migration finishes.
How does Filament run a zero-downtime Postgres migration?
Filament creates the replication slot with an exported snapshot, reads every selected table in full through that snapshot, and replays the stream from the same point, so baseline and changes share one consistent position. Full reads shard into up to 64 parallel ranges and checkpoint, so an interrupted run resumes instead of restarting.
Does Filament need Kafka or a separate cluster to do this?
No. It runs as one binary on a laptop, a Go library inside your own service, or a Helm chart on Kubernetes. CDC runs need the Postgres datastore for durable stream positions, but there is no message broker and no multi-container stack to operate.
More articles
Questions
Answered
FAQ
What does Galaxy do?
What is Filament?
What is enterprise context management?
What does working with Galaxy look like?
How do you handle security and compliance?
Why does Galaxy build in the open?
Company
Copyright © 2026 Galaxy. All rights reserved.