Streaming Replication and Read Replicas in PostgreSQL
- postgresql
- database
- replication
- read-replicas
- scaling
- backend
Once a single database can't keep up with read traffic, or you need a standby ready to take over if the primary fails, replication is the answer. PostgreSQL copies changes from a primary server to one or more replicas in near real time, giving you extra copies you can read from and fail over to. Replication is the foundation of both read scaling and high availability, and understanding how it works (and its one unavoidable catch, replication lag) is what lets you use replicas without introducing subtle bugs.
This is part 34 of the PostgreSQL Masterclass, following connection pooling.
How streaming replication works
The primary writes every change to its write-ahead log, the WAL, before applying it, which is the same mechanism that gives durability. Streaming replication has replicas connect to the primary and continuously receive that WAL stream, replaying each change to stay in sync. The result is one or more standby servers that mirror the primary, a fraction of a second behind.
Replicas are read-only. They replay the primary's changes and can serve SELECT queries, but they can't accept writes, because all writes must go through the primary to keep a single source of truth. This shape (one writable primary, several read-only replicas) is the standard building block.
Read replicas for scaling reads
Most applications read far more than they write. Read replicas exploit that: point your read-heavy queries (reports, dashboards, search, anything that only reads) at replicas, and keep writes and read-your-own-write paths on the primary. You've multiplied read capacity by adding replicas, while the primary handles the comparatively smaller write load.
The application routes queries deliberately. A common pattern is two connection pools: one to the primary for writes and consistency-critical reads, one to replicas for everything else. Some frameworks and poolers can route automatically based on whether a statement writes, but explicit routing gives you control over the cases that matter.
The catch you must design for: replication lag
Replicas are slightly behind the primary. Usually milliseconds, but under load or long transactions it can grow to seconds. This lag creates the classic bug: a user updates their profile (write to primary), the app immediately reads it back (from a replica that hasn't received the change yet), and the user sees their old data. It looks like the save failed.
This is the read-your-own-writes problem, and you design around it rather than eliminate it:
- Route reads that must reflect a just-made write to the primary. After a user changes something and you show it back to them, read from the primary for that request.
- Send only lag-tolerant reads to replicas. Reports, other users' public data, search results, analytics: places where being a second behind is invisible.
- Monitor the lag so you know when it's abnormal. You can measure it directly:
-- run on the replica: how far behind is it, in bytes and time
SELECT
pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
now() - pg_last_xact_replay_timestamp() AS replication_delay;
Designing which reads tolerate lag and which don't is the real work of using replicas. Get that split right and replicas are a clean scaling win; get it wrong and you ship confusing "my change disappeared" bugs.
Synchronous vs asynchronous replication
By default, replication is asynchronous: the primary commits and returns success without waiting for replicas to receive the change. It's fast, but if the primary fails before a replica got the latest commits, those commits are lost.
Synchronous replication makes the primary wait for at least one replica to confirm receipt before reporting a commit as successful. This guarantees no committed data is lost on failover, at the cost of higher write latency (every commit waits for a network round trip to a replica) and a risk that writes stall if the synchronous replica is down. You choose per your durability needs:
-- primary waits for one named standby to confirm each commit
synchronous_standby_names = 'replica1';
Most systems run asynchronous replication and accept a tiny window of potential loss on failover. Synchronous is for workloads where losing even a few committed transactions is unacceptable, like financial systems, and it's often configured to require confirmation from one of several replicas to avoid stalling.
Managed replicas
On managed Postgres (AWS RDS, Aurora, Cloud SQL, and others), read replicas are a few clicks: the service handles WAL streaming, replica provisioning, and often the connection endpoints. Aurora goes further with a shared-storage design that reduces lag. The concepts are identical to self-managed replication, so the design work (routing lag-tolerant reads to replicas, keeping read-your-own-writes on the primary) is the same regardless of who runs the servers.
Replication turns one database into a small fleet: a primary for writes and a set of replicas for reads and standby. The mechanics are handled for you by Postgres or your provider; the engineering is deciding which reads can tolerate lag and routing them accordingly. Do that split thoughtfully and you scale reads and gain a failover standby at the same time.
Next, we build on replication to cover high availability specifically: automatic failover, promoting a replica when the primary dies, and keeping the application connected through it.
Key takeaways
- Streaming replication ships the primary's WAL to read-only replicas that replay it to stay in near real-time sync.
- Read replicas scale reads: route read-heavy, lag-tolerant queries to replicas and keep writes on the single primary.
- Replicas lag slightly behind, causing read-your-own-writes bugs; route reads that must reflect a just-made write to the primary.
- Asynchronous replication (the default) is fast but can lose the latest commits on failover; synchronous guarantees no loss at higher write latency.
- Monitor replication lag so you can detect when it grows beyond your tolerance.
Frequently asked questions
What is streaming replication in PostgreSQL?
A mechanism where replicas continuously receive the primary's write-ahead log and replay it, staying in near real-time sync. The replicas are read-only copies used for scaling reads and for standby during failover.
How do read replicas scale a database?
By serving read queries from copies of the data, so the primary handles mostly writes. Since most applications read far more than they write, routing read-heavy queries to replicas multiplies read capacity.
What is replication lag and why does it matter?
Replicas are slightly behind the primary, usually milliseconds but sometimes seconds. If you write to the primary then immediately read from a replica, you may see stale data (the read-your-own-writes problem), so reads that must reflect a recent write should go to the primary.
What's the difference between synchronous and asynchronous replication?
Asynchronous replication (the default) lets the primary commit without waiting for replicas, which is fast but can lose the latest commits if the primary fails. Synchronous replication waits for a replica to confirm each commit, guaranteeing no loss at the cost of higher write latency.
How do I check replication lag?
On the replica, compare received and replayed WAL positions and check `now() - pg_last_xact_replay_timestamp()` for the delay in time. Monitor this so you're alerted when lag grows beyond your application's tolerance.
Further reading
Explore more on

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.