1. Courses
  2. System Design
  3. Databases at Scale
  4. Stop 2

35 minIntermediate

Replication and read replicas

A replica is a second Postgres server that keeps a live copy of your database. It can take heavy reads off the primary and stand ready to take over if the primary dies. It is always a little behind, though, and this lesson shows you how to design for that.

By the end you can

  • Explain the WAL and streaming replication in plain words
  • Choose between asynchronous and synchronous replication, knowing what each one costs
  • Spot the read-your-own-writes bug and decide which reads must stay on the primary
  • Create a replica with pg_basebackup -R and measure its lag with SQL
  • Route reads in Prisma with the read-replicas extension and $primary()

The everyday picture

Head office keeps the master ledger. Every time a clerk writes an entry, she also writes one line in a diary: "page 41, line 7: add 12 cartons of milk". A runner carries the new diary lines to two branch offices in other cities all day long. The branch clerk copies each line into the branch's own ledger, in the same order.

The branches can answer "what did store 27 sell last month?" without calling head office, but they never write entries themselves. Their copy is always a little behind, because the runner needs time to travel and the clerk needs time to copy.

If head office floods in the monsoon, a branch can become the new head office, because its ledger is almost complete. And if a head office clerk writes something wrong, the runner carries the mistake to every branch too. Postgres replication works this way, line by line.

One primary, several read-only copies

In a replicated setup one server is the , the only one that accepts INSERT, UPDATE and DELETE. The others are , also called standbys. They hold a full copy of the data and answer SELECT queries. A write on a replica fails with ERROR: cannot execute INSERT in a read-only transaction.

Teams add replicas for three reasons:

  • Spread the reads. Reports, dashboards and searches move to a replica, so the primary keeps its CPU and disk for writes.
  • Isolate heavy work. A 40-second month-end report on the primary competes with every upload from the store tills (the POS). On a replica it only slows other report users.
  • Survive a failure. If the primary's machine dies, a replica can become the new primary in seconds or minutes, instead of a restore that takes hours.

Replicas do not add write capacity. Every replica must apply every write the primary makes, so writes still go through one machine. If writes are your limit, look at Partitioning big tables and Sharding and consistent hashing.

Quick check

The primary sits at 90% CPU, and almost all of that work is till uploads, which are writes. Will two new read replicas fix it?

The WAL: a diary that every copy replays

Before Postgres changes any data file, it appends a short record of the change to the , the write-ahead log. A COMMIT is safe once its WAL records are flushed to disk; the table files can be updated later. After a crash, Postgres replays the WAL from the last checkpoint, so no committed work is lost.

A replica is a server that never finishes this crash recovery. It starts from a copy of the primary's files, then keeps receiving the primary's WAL over the network and replaying it, record after record. This is . A walsender process on the primary sends; a walreceiver on the replica receives.

Every WAL record has a position called the . For each replica, Postgres tracks how far it has written, flushed to disk and replayed the WAL. Queries on the replica see only replayed changes. The gap between the primary's newest LSN and the replica's replayed LSN is the .

NestJS API Dashboards writes Primary WAL at #1,000 WAL stream WAL stream report reads, big queries Replica 1 read-only replayed #998 ~10 ms behind Replica 2 read-only replayed #640 ~3 s behind #n = WAL position (LSN)
All writes go to the primary. Each replica receives the WAL stream and replays it in order. Replica 2 serves heavy reports, so it is busier and has replayed fewer records. Lag is the distance between where the primary is and where a replica has replayed to.

Quick check

A replica has received WAL up to position 1,000 and flushed it to disk, but has replayed only up to 940. What can queries on the replica see?

In one data centre, lag is usually milliseconds. It grows when the replica cannot keep up: replay runs in a single process, so many parallel writers on the primary can produce WAL faster than one replica applies it, and a smaller replica machine makes this worse.

Async or sync: when is a write "done"?

By default replication is . When your API commits, the primary flushes the WAL to its own disk and replies "done". The WAL goes to the replicas a moment later. Commits stay fast, and a slow replica never slows the primary. The cost: if the primary's disk is destroyed, the last few milliseconds (or, if a replica was lagging, seconds) of committed sales may never have reached any replica. After a failover they are gone.

With replication you list replicas in synchronous_standby_names, and each COMMIT waits until a replica confirms. A confirmed sale then exists on two machines. The price comes in two forms:

  • Every commit pays a network round trip. In one data centre that is often well under a millisecond. To a replica in another city it can be tens of milliseconds on every single commit.
  • If the synchronous replica goes down, commits stop. Postgres does not fall back to async on its own. Every COMMIT waits (you can see the wait event SyncRep in pg_stat_activity) until the replica returns or someone changes the setting. The usual protection is two candidate replicas with ANY 1 (replica1, replica2), so either one can confirm.

The setting synchronous_commit decides what "confirms" means, and you can change it per transaction with SET LOCAL:

synchronous_commitCOMMIT returns whenGood for
local (or no sync replicas listed)The primary's disk has the WALMost apps; fastest safe default
remote_writeThe replica has received it and handed it to its operating systemSurvives a primary crash, cheaper than on
on (the default, with sync replicas listed)The replica has flushed it to its diskPayments, stock moves you must never lose
remote_applyThe replica has replayed it, so queries there can see itRare; when reads on the replica must see the write

Most teams start async and add a synchronous standby in the same data centre only when the business says losing even one second of sales is unacceptable. That is a decision about (Backups and disaster recovery).

Quick check

Replication is asynchronous, the default. The primary's disk is destroyed one second after a COMMIT returned "done", and you promote a replica. What may be missing on it?

Replication lag and the read-your-own-writes bug

Lag causes one classic bug. A user saves something, the app reads it back from a replica, and the replica has not replayed the save yet. The user sees old data and thinks the save failed.

App Primary Replica 1. POST /grn 2. 201 saved WAL, ~300 ms 3. GET /grn list 4. old list replica has it now GRN missing, so the manager saves it again Fix: read from the primary after a write
Time runs downward. The save reaches the primary at once, but the WAL needs about 300 ms to reach and be replayed on the replica. The list request in step 3 arrives earlier, so the replica answers with the old list. Without an idempotency key (APIs done right) the second save creates a duplicate GRN.

The fix is to decide, read by read, how fresh the answer must be.

Must stay on the primary:

  • Any read the user makes right after their own write: the list after a save, the order screen after "Place order".
  • A read that decides a write: "is there stock left?", "is this voucher already used?". These belong inside a transaction on the primary anyway (Transactions, ACID, isolation and locks).
  • Security data: logins, roles, a revoked device. A replica that is 5 seconds behind would let a blocked user in for 5 more seconds.
  • Payment and order status that the customer is waiting for.

Fine on a replica: dashboards, reports, exports, product catalogue and search, history older than a minute, anything the screen already labels "updated a moment ago".

For reads right after a user's own write, three patterns cover most cases: return the saved object in the POST response, send that one follow-up read to the primary, or send all of that user's reads to the primary for a few seconds. The lesson CAP, PACELC and consistency builds the last one with Redis.

Think first: the reporting replica runs a 2-minute month-end query. Meanwhile the primary runs VACUUM on the same table. What happens on the replica?

VACUUM on the primary removes old row versions, and that removal travels to the replica in the WAL. But the long query on the replica may still need those old rows. Replay has to choose: wait, or cancel the query. Postgres waits up to max_standby_streaming_delay (30 seconds by default). During that wait replay stops, so lag grows for every reader of this replica. After 30 seconds it cancels the query with ERROR: canceling statement due to conflict with recovery.

On a reporting replica you can raise that delay, or set hot_standby_feedback = on so the primary keeps the rows the replica still needs, at the cost of dead rows piling up on the primary (Running Postgres in production). Many teams accept more lag on the reporting replica and keep a separate replica with default settings for failover.

Failover: when the primary dies

has three steps. First, make sure the old primary is really down and cannot come back. Second, the most up-to-date replica with SELECT pg_promote();. Third, point the applications at the new primary and let them reconnect.

The first step is the dangerous one. Say the primary did not die; only the network to it dropped for a minute. You promote the replica, the network comes back, and now some app servers write to the old primary and some to the new one. This is : two versions of the same receipts and stock moves that nobody can merge automatically. Every failover plan must say how the old primary is stopped: shut it down, block it at the firewall, or let a tool fence it.

  • Manual promotion fits a small team with one replica, if the steps are written down and practised. Expect minutes of downtime, mostly spent noticing and deciding.
  • Patroni is the common open-source tool for automatic failover. A Patroni agent beside each Postgres server holds or watches a single leader lock in etcd or Consul. Only the lock holder runs as primary, and a primary that cannot renew its lock demotes itself, which prevents split brain.
  • Managed services such as Amazon RDS, Google Cloud SQL and Azure Database for PostgreSQL detect the failure, promote a standby and move a DNS name for you, usually within a minute or two. On some of them the failover standby cannot serve reads; read replicas are a separate feature.

Whatever you choose, connections to the old primary break and in-flight transactions fail, so the API needs timeouts, reconnects and safe retries (Failure is normal: timeouts, retries, backoff and circuit breakers). Afterwards, pg_rewind can turn the old primary into a replica of the new one without a full copy.

Logical replication: copy chosen tables to another database

Streaming replication copies the whole server, byte for byte. The replica must run the same major version, has exactly the same tables and indexes, and is read-only. works at the level of rows instead. The primary publishes changes for chosen tables, and another database subscribes to them.

The subscriber is a normal, writable database. It can have its own report indexes and tables, and even a newer Postgres version. So logical replication is used to feed a reporting database, to feed other systems through change data capture (Beyond SQL: choosing the right data store), and to upgrade major versions with little downtime.

The limits: schema changes (DDL) and sequence values are not replicated, so migrations run on both sides; tables need a primary key; and it uses a , which keeps WAL on the primary until the subscriber reads it.

Build it

We will turn a second server into a streaming replica, measure its lag, and route reads from NestJS with Prisma. The commands use the Ubuntu packages for PostgreSQL 16, with the primary at 10.0.1.10 and the new replica at 10.0.1.11. Try it on two test machines or two local clusters on different ports. Replication cannot run in a browser, but the two Practice exercises below, choosing where a read goes and measuring lag in bytes, can.

  1. Prepare the primary

    The defaults since PG 10 already allow replication (wal_level = replica, 10 WAL senders, 10 slots), so you mostly add a limit for slots and a login for replicas.

    INI
    # /etc/postgresql/16/main/postgresql.conf on the PRIMARY
    listen_addresses       = '10.0.1.10'   # private address only
    wal_level              = replica       # default; 'logical' if you also need step 6
    max_wal_senders        = 10            # default; one per replica plus spare
    max_replication_slots  = 10            # default
    max_slot_wal_keep_size = '50GB'        # a dead replica's slot may not fill the disk
    SQL
    -- On the primary: a role that may only replicate
    CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'change-me';
    Output
    # /etc/postgresql/16/main/pg_hba.conf on the PRIMARY
    # TYPE  DATABASE     USER        ADDRESS         METHOD
    host    replication  replicator  10.0.1.11/32    scram-sha-256

    Reload with sudo systemctl reload postgresql. A change to wal_level, max_wal_senders or max_replication_slots needs a restart instead.

  2. Clone the primary with pg_basebackup -R

    On the new replica machine only. -R writes the file standby.signal and the connection settings, so the copy starts as a replica. -C -S replica1 creates a replication slot, so the primary keeps the WAL this replica still needs.

    Terminal
    # On the NEW REPLICA machine (10.0.1.11)
    sudo systemctl stop postgresql
    sudo -u postgres mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.empty
    
    sudo -u postgres pg_basebackup \
      -h 10.0.1.10 -U replicator \
      -D /var/lib/postgresql/16/main \
      -R -X stream -C -S replica1 -P   # prompts for the replicator password
    
    # Give the replica a name so it shows up by name on the primary
    echo "cluster_name = 'replica1'" | sudo tee -a /etc/postgresql/16/main/postgresql.conf
    sudo systemctl start postgresql
    
    sudo -u postgres ls /var/lib/postgresql/16/main | grep -E 'signal|auto'
    Output
    postgresql.auto.conf     <- now holds primary_conninfo and primary_slot_name
    standby.signal           <- "start as a replica"
  3. Check the replica from both sides

    On the primary, pg_stat_replication has one row per connected replica. The output below was captured on a test pair under a write benchmark, with the replica deliberately delayed by 3 seconds.

    SQL
    -- On the PRIMARY
    SELECT application_name AS replica, state, sync_state,
           pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS bytes_behind,
           write_lag, flush_lag, replay_lag
    FROM pg_stat_replication;
    Output
     replica  |   state   | sync_state | bytes_behind |    write_lag    |    flush_lag    |   replay_lag
    ----------+-----------+------------+--------------+-----------------+-----------------+-----------------
     replica1 | streaming | async      | 2802 kB      | 00:00:00.000804 | 00:00:00.000804 | 00:00:02.998796

    Read it like this: the WAL reached the replica's disk in under a millisecond, but replay is 3 seconds behind. The *_lag columns describe recent WAL only, and go NULL a while after the primary goes idle.

    SQL
    -- On the REPLICA
    SELECT pg_is_in_recovery() AS is_replica,
           CASE WHEN pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()
                THEN interval '0'            -- caught up: nothing left to replay
                ELSE now() - pg_last_xact_replay_timestamp()
           END AS replay_delay;

    Keep the CASE. Without it, the delay keeps growing whenever the primary has no new writes, say at 4 am, and pages you for lag that does not exist.

  4. Watch the slots

    A slot whose replica is gone keeps WAL on the primary until the disk fills and the primary stops. Alert on this, and drop slots you no longer need.

    SQL
    SELECT slot_name, slot_type, active, wal_status,
           pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_kept
    FROM pg_replication_slots;
    
    -- Only after you are sure nobody uses it:
    -- SELECT pg_drop_replication_slot('old_replica');

    Good alert starting points: replay_lag above 30 seconds for 5 minutes, any slot that is inactive for more than an hour, and wal_status other than reserved.

  5. Route reads in NestJS with Prisma

    Install @prisma/extension-read-replicas. Version 0.4.1 targets Prisma 6, 0.3.x and 0.4.0 target Prisma 5, and 0.5 needs Prisma 7, which configures replicas differently. Reads outside a transaction go to the replica. Writes, $transaction and raw SQL go to the primary. $primary() forces a read to the primary.

    TypeScript
    // db.module.ts
    import { Global, Module } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';
    import { readReplicas } from '@prisma/extension-read-replicas';
    
    export const DB = 'DB';
    const createDb = () =>
      new PrismaClient().$extends(
        readReplicas({ url: process.env.DATABASE_REPLICA_URL! }), // an array also works
      );
    export type Db = ReturnType<typeof createDb>;
    
    @Global()
    @Module({ providers: [{ provide: DB, useFactory: createDb }], exports: [DB] })
    export class DbModule {}
    TypeScript
    // grn.service.ts
    @Injectable()
    export class GrnService {
      constructor(@Inject(DB) private readonly db: Db) {}
    
      // Report: a few seconds old is fine, so this goes to the replica
      salesBySite(from: Date, to: Date) {
        return this.db.sale.groupBy({
          by: ['siteId'],
          where: { saleDate: { gte: from, lt: to } },
          _sum: { total: true },
        });
      }
    
      // The list shown right after a save: must be fresh, so force the primary
      recentGrns(siteId: bigint) {
        return this.db.$primary().grn.findMany({
          where: { siteId },
          orderBy: { receivedAt: 'desc' },
          take: 20,
        });
      }
    }

    Raw SQL always goes to the primary, because the extension cannot tell a read from a write. For a heavy raw report, opt in to the replica with this.db.$replica().$queryRaw`...`.

  6. Optional: feed a reporting database with logical replication

    This needs wal_level = logical on the primary (restart required). Create the same table on the reporting database first, because DDL is not copied.

    SQL
    -- On the PRIMARY
    CREATE PUBLICATION reporting_pub FOR TABLE grn, sale
      WITH (publish_via_partition_root = true);   -- because sale is partitioned
    GRANT SELECT ON grn, sale TO replicator;       -- needed for the first copy
    
    -- On the REPORTING database (tables already created)
    CREATE SUBSCRIPTION reporting_sub
      CONNECTION 'host=10.0.1.10 dbname=mangrove user=replicator password=change-me'
      PUBLICATION reporting_pub;

    Without the GRANT, the first copy never arrives, and the subscriber's log repeats permission denied for table grn.

  7. Practise a failover on the test pair

    Do this on test machines until the steps are boring. Then write them into the runbook.

    Terminal
    # 1. Stop the old primary so it can never take writes again
    sudo systemctl stop postgresql            # on 10.0.1.10
    
    # 2. Promote the replica
    sudo -u postgres psql -c "SELECT pg_promote();"                 # on 10.0.1.11
    sudo -u postgres psql -Atc "SELECT pg_is_in_recovery();"        # f = it is a primary now
    
    # 3. Point DATABASE_URL at 10.0.1.11 (or move the DNS name) and restart the API

Bring it to your team

Ask these when someone proposes a replica, or when "the data didn't show up" bugs appear.

  • "Which reads go to the replica and which stay on the primary? Can we see that decision per endpoint?"
  • "What is the replica lag on a normal evening and at the festival peak, and which alert fires at what number?"
  • "After a user saves, which screen do they see next, and where does that screen read from?"
  • "If the primary dies at 8 pm, who promotes the replica, how long does it take, and how does the API find the new primary?"
  • "What stops the old primary from accepting writes again after a failover?"
  • "Is replication async or sync, and how much recent data could we lose in a failover?"

Red flags

  • "We have replicas, so we don't need backups."
  • Every read sent to the replica, including the screen shown right after a save.
  • Synchronous replication to a single replica, and nobody knows that commits stop when it is down.
  • A replication slot for a replica or tool that was removed months ago.
  • A failover plan that has never been practised on a test system.

Practice

Decide where each read goes

Write routeRead(read, ctx) for the Store app's API. It returns 'primary' or 'replica'. A read has a kind ('auth', 'decides-write' or 'normal') and maxStaleMs, the most lag this screen can accept. ctx has nowMs, lastWriteMs (this user's last write, or null) and replicaLagMs (the measured lag, or null if unknown). The rules, in order:

  1. Auth reads and reads that decide a write always go to the primary.
  2. A user who wrote less than STICKY_MS (5,000 ms) ago reads from the primary.
  3. If the lag is unknown, or bigger than the screen's maxStaleMs, use the primary.
  4. Otherwise, use the replica.
JavaScript exerciseRuns in your browser with tests
const STICKY_MS = 5000; // a user's own write pins them to the primary this long

function routeRead(read, ctx) {
  // read: { kind: 'auth' | 'decides-write' | 'normal', maxStaleMs }
  // ctx:  { nowMs, lastWriteMs (or null), replicaLagMs (or null) }
  return 'replica';
}
Ctrl + Enter runs
Show a solution
JavaScript
const STICKY_MS = 5000; // a user's own write pins them to the primary this long

function routeRead(read, ctx) {
  if (read.kind === 'auth' || read.kind === 'decides-write') return 'primary';
  if (ctx.lastWriteMs !== null && ctx.nowMs - ctx.lastWriteMs < STICKY_MS) return 'primary';
  if (ctx.replicaLagMs === null || ctx.replicaLagMs > read.maxStaleMs) return 'primary';
  return 'replica';
}
Pass the exercise to finish this task.

Measure lag in bytes from two LSNs

An LSN such as 16/B374D848 is a 64-bit byte position in the WAL, written as two hexadecimal halves: the part before the slash is the high 32 bits, the part after it the low 32 bits. Write lagBytes(primaryLsn, replayLsn), which returns how many bytes of WAL the replica still has to replay, like pg_wal_lsn_diff does in SQL. Plain numbers are precise enough here.

JavaScript exerciseRuns in your browser with tests
function lsnToBytes(lsn) {
  // '16/B374D848' -> high * 2^32 + low
}

function lagBytes(primaryLsn, replayLsn) {
  // bytes the replica is behind
}
Ctrl + Enter runs
Show a solution
JavaScript
function lsnToBytes(lsn) {
  const [high, low] = lsn.split('/');
  return parseInt(high, 16) * 2 ** 32 + parseInt(low, 16);
}

function lagBytes(primaryLsn, replayLsn) {
  return lsnToBytes(primaryLsn) - lsnToBytes(replayLsn);
}
Pass the exercise to finish this task.

Check yourself

The API writes to the primary and reads lists from an async replica. Sometimes a freshly saved GRN is missing from the list for a moment. What is the best fix?

At 10:42 someone runs DELETE FROM article; without a WHERE on the primary. You have two streaming replicas. What is the situation?

synchronous_standby_names = 'replica1' is set, and the replica1 server loses power. What happens to new commits on the primary?

Flashcards

Open my review

Key takeaways

  • The primary takes every write. Replicas replay its WAL and serve reads. They do not add write capacity.
  • Async is the default and keeps commits fast. Sync protects every commit, costs a round trip each time, and stops commits if the sync replica is down.
  • Replicas are always somewhat behind. Keep reads that follow a user's own write, reads that decide a write, and security checks on the primary.
  • Measure lag with pg_stat_replication on the primary and the replay timestamp on the replica, and watch replication slots.
  • Failover is promote plus fencing the old primary. Practise it. And remember that replicas copy mistakes, so you still need backups.
Episode transcriptThe copy that is always a little behind · about 13 minutes · tap any line to play from there
  1. NadiaA store manager saves a GRN, a goods received note. He looks at the list on his screen, and it isn't there. So he saves it again.
  2. ImranAnd now finance has two GRNs for one delivery. Nadia, I've heard that exact complaint from our stores.
  3. NadiaNothing crashed, either. The first save worked. The list was read from a copy of the database that was about a third of a second behind.
  4. NadiaThat copy is called a replica, and it's today's topic. Why teams add them, why they're always a little behind, and how to stop that delay from fooling your users.
  5. ImranA third of a second, and we get a duplicate. Okay, I'm listening.
  6. NadiaPicture head office keeping the master ledger. Every time a clerk writes an entry, she also writes one short line in a diary. Something like: page forty-one, line seven, add twelve cartons of milk.
  7. NadiaA runner carries the new diary lines to two branch offices in other cities, all day long. The branch clerk copies each line into the branch's own ledger, in the same order.
  8. ImranSo a branch can answer what store twenty-seven sold last month, without phoning head office. But its copy is a bit behind, because the runner has to travel.
  9. NadiaYes. Branches never write entries themselves. If head office floods in the monsoon, a branch can take over, because its ledger is almost complete. And remember this one: if a head office clerk writes something wrong, the runner carries that mistake to every branch too.
  10. NadiaIn Postgres, head office is the primary, the only server that accepts inserts, updates and deletes. The branches are replicas. They hold a full copy and answer select queries. Try to write to one and it refuses.
  11. ImranAnd teams add them to take reports off the primary?
  12. NadiaThat's one reason. A forty-second month-end report on the primary competes with every POS upload. On a replica, it only slows the other report users. The other big reason is survival. If the primary's machine dies, a replica can take over in seconds or minutes, instead of a restore that takes hours.
  13. ImranThen here's my festival plan. Till uploads are heavy, so I add three replicas and get three times the write capacity.
  14. NadiaThat's the trap, Imran. Replicas add no write capacity at all. Every replica has to apply every write the primary makes, so all writes still go through one machine. If writes are your limit, you need a bigger machine, less write work, or sharding, which is the next episode.
  15. ImranFair. Replicas help with reads and survival. Writes stay on one box. So what's the diary, technically?
  16. NadiaThe WAL, the write-ahead log. Before Postgres changes any data file, it appends a short record of the change to the WAL. A commit is safe once those records are on disk. After a crash, Postgres replays the WAL, so no committed work is lost.
  17. NadiaA nice way to remember a replica: it's a server that never finishes crash recovery. It keeps receiving the primary's WAL over the network and replaying it, record after record. That's streaming replication.
  18. ImranAnd lag is how far behind the replay is.
  19. NadiaYes. If you can see your screen, the first diagram shows it. If not, picture the primary at WAL position one thousand, streaming to two replicas. Replica one has replayed up to nine hundred ninety-eight, about ten milliseconds behind. Replica two is only at six hundred forty, about three seconds behind.
  20. ImranWhy is replica two so far back?
  21. NadiaIt serves the heavy reports, so it's busier. And replay runs in a single process, so many writers on the primary can produce WAL faster than one replica applies it. In one data centre, lag is usually milliseconds, but it can grow.
  22. ImranWhen my API commits a sale, when does the primary actually say done?
  23. NadiaBy default, replication is asynchronous. The primary puts the WAL on its own disk and replies done. Replicas get it a moment later. Commits stay fast. The cost: if the primary's disk is destroyed, the last few milliseconds of sales, or seconds if a replica was lagging, may never have reached any replica.
  24. ImranSo I switch on synchronous replication, and every sale is safe on two machines.
  25. NadiaIt is, but you pay twice. Every commit waits for a network round trip. In one data centre that's often well under a millisecond, but to a replica in another city it can be tens of milliseconds on every commit. And if the synchronous replica goes down, commits stop.
  26. ImranReally? Postgres doesn't notice and quietly fall back to async?
  27. NadiaNever on its own. It keeps its promise, so every commit waits, and clients hang until the replica returns or someone changes the setting. The usual protection is to list two candidate replicas and let either one confirm.
  28. ImranWould synchronous replication have fixed my duplicate GRN?
  29. NadiaWith the default setting, no, and that's a common mistake. The default waits until the replica has flushed the WAL to its disk, not until it has replayed it. So the list could still miss the GRN, and every commit would be slower.
  30. ImranSo most teams just stay async?
  31. NadiaThey start async, and add a sync standby in the same data centre only when the business says losing even one second of sales is unacceptable.
  32. NadiaLet's replay the GRN story with the second diagram. Time runs downward, with three columns: the app, the primary and the replica. Step one, the app saves the GRN on the primary. Step two, the primary says saved. The WAL needs about three hundred milliseconds to reach the replica and be replayed.
  33. NadiaStep three, the app asks the replica for the list, and that request arrives first. Step four, the replica returns the old list. The manager sees nothing and saves again. Without an idempotency key, from the APIs lesson, you get the duplicate.
  34. ImranSo the fix is to decide, read by read, how fresh each answer must be.
  35. NadiaYes. Some reads must stay on the primary. Any read right after the user's own write. Any read that decides a write, like, is there stock left, or, is this voucher already used. Security data, like logins and a revoked device, because a replica five seconds behind lets a blocked user in for five more seconds.
  36. ImranAnd dashboards, reports, exports, the catalogue and search can go to the replica.
  37. NadiaExactly. For the read right after a save, return the saved object in the save response, or send that one follow-up read to the primary, or send all that user's reads to the primary for a few seconds.
  38. ImranAt Mangrove, I'm thinking of the festival sales dashboard in the back office.
  39. NadiaOn a festival evening, about three hundred managers keep it open, and it refreshes every thirty seconds. Each refresh is a two-second query. That's about ten heavy queries a second on the primary, in the same hours when the tills in all three hundred and twenty stores upload sales.
  40. ImranSo we move it to a replica, and put a line on the screen saying data as of a certain time.
  41. NadiaYes, then nobody mistakes five seconds of lag for a missing sale. The GRN entry screen and the stock check stay on the primary.
  42. ImranNow the scary part. The primary dies at eight at night on the eve of a festival.
  43. NadiaFailover has three steps. Make sure the old primary is really down and can't come back. Promote the most up-to-date replica. Point the applications at it.
  44. ImranI'd guess promoting is the hard part.
  45. NadiaThe first step is the dangerous one. Say the primary didn't die, only the network to it dropped for a minute. You promote the replica, the network comes back, and now some app servers write to the old primary and some to the new one.
  46. ImranTwo head offices, each writing in its own ledger.
  47. NadiaThat's split brain. Two versions of the same receipts and stock moves that nobody can merge automatically. So every failover plan must say how the old primary is stopped: shut it down, block it at the firewall, or let a tool like Patroni fence it. In Patroni, only the holder of one leader lock may act as primary.
  48. NadiaAnd whatever you choose, connections break during failover, so the API needs timeouts, reconnects and safe retries.
  49. ImranSo what does the Build it section have me do?
  50. NadiaIt turns a second server into a replica on Postgres sixteen. On the primary you create a login that may only replicate, and set a cap so a dead replica can't make the primary fill its disk.
  51. NadiaOn the new machine you run pg base backup with the dash R option, which clones the primary and makes the copy start life as a replica.
  52. ImranAnd then I check that it's actually keeping up.
  53. NadiaFrom both sides. The primary has a view with one row per replica, showing its lag. On the replica, you compare how much WAL it has received with how much it has replayed. If they match, lag is zero. Keep that check, or at four in the morning, when nothing is written, the lag seems to grow and pages you for nothing.
  54. NadiaThen you route reads in NestJS with Prisma's read replicas extension. Reads go to the replica, writes go to the primary, and the list right after a save is forced to the primary. Last, you practise a failover on test machines until it's boring.
  55. ImranReplication won't run in a browser, though.
  56. NadiaNo, but the reasoning does. The practice section has you write a small function that decides, read by read, primary or replica, using the rules we just talked about. A second one turns two WAL positions into bytes of lag.
  57. ImranMy design review questions, then. Which reads go to the replica, and can we see that per endpoint? What's the lag on a normal evening and at the festival peak, and which alert fires at what number? After a user saves, where does the next screen read from?
  58. NadiaAnd who promotes the replica at eight at night, and what stops the old primary taking writes again? Good set.
  59. NadiaNow a puzzle. Your reporting replica runs a two-minute month-end query. Meanwhile the primary runs VACUUM on the same table, cleaning out old row versions. What happens on the replica? Take a second.
  60. ImranI'd say the replica finishes the report and applies the cleanup afterwards.
  61. NadiaClose, but it won't wait forever. The cleanup arrives in the WAL, and the long query may still need those old rows. Postgres waits up to thirty seconds by default, and while it waits, replay stops, so lag grows for every reader. After thirty seconds, it cancels the query.
  62. ImranSo for a reporting replica, I'd allow a longer wait, and accept more lag there.
  63. NadiaYes, and many teams keep a separate replica with default settings for failover.
  64. ImranLet me say it back. One. The primary takes every write. Replicas replay its diary and serve reads, and add no write capacity.
  65. ImranTwo. Async keeps commits fast. Sync protects every commit, but costs a round trip each time, and stops commits if your only sync replica dies.
  66. ImranThree. What the user just wrote, what decides a write, and security checks stay on the primary.
  67. NadiaAll good. One addition from the ledger story: the runner carries mistakes too. If someone deletes the whole article table at ten forty-two, every replica replays that delete within milliseconds. Replicas protect you from a dead machine, not from a bad change. You still need backups.
  68. ImranAnd if writes are ever my real limit, replicas won't save me.
  69. NadiaRight. Next episode is that limit: splitting one database across servers, and why opening a fourth distribution centre can move most stores overnight if you pick the wrong rule.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Read-heavy with a cache

A cache and a read replica take repeated reads off the primary.

Open in the System Design Studio
Next stopSharding and consistent hashing
Was this lesson helpful?
Questions and discussion

Loading the discussion…