Saltar al contenido principal
Version: 1.0.0

Read replicas

For read-heavy workloads you can spread reads across one or more replica databases while writes continue to go to the primary. It is opt-in — without WithReplicas, every operation uses the single primary connection, unchanged.

If a replica goes down, a read routed to it fails over to the primary automatically and the replica is taken out of rotation for a cooldown (default 5s, tunable with WithReplicaDownCooldown), after which it is retried — so a downed replica degrades performance, not correctness.

client, err := quark.New("pgx", primaryDSN,
quark.WithReplicas(replica1DSN, replica2DSN),
quark.WithMaxOpenConns(16),
)

New opens one connection pool per replica DSN (same pool options and dialect as the primary) and pings each. Close closes them all.

What routes where

  • Reads route to a replica — both multi-row reads (List, Iter, eager-loading) and single-row reads (First, Find, Count, and the aggregates Sum/Avg/Min/Max).
  • Writes (Create, Update, UpdateFields, Delete) always go to the primary, including the write paths that read a row back (INSERT ... RETURNING, MSSQL SCOPE_IDENTITY()).
  • Reads inside Client.Tx use the transaction's connection (the primary), so they always see the transaction's own writes.
  • Reads under RowLevelSecurityNative stay on the primary — the policy is evaluated on the connection that set the session variable, so the read must not move to another pool.

Selection strategy

When more than one replica is configured, the strategy decides which healthy replica serves each read. Set it with WithReplicaStrategy; the default is round-robin:

client, err := quark.New("pgx", primaryDSN,
quark.WithReplicas(replica1DSN, replica2DSN),
quark.WithReplicaStrategy(quark.ReplicaLeastConn),
)
  • ReplicaRoundRobin (default) — advances an atomic cursor one slot per read; the most even distribution under steady concurrency.
  • ReplicaRandom — picks a replica at random (uniform across healthy replicas); no shared cursor, so it avoids round-robin's single contended atomic.
  • ReplicaLeastConn — picks the replica with the fewest in-use pool connections; best when replica query latencies are uneven.

Every strategy honours the failover cooldown below: a replica taken out of rotation is never chosen until its cooldown expires.

Consistency: stale reads and Sticky

Replicas are typically replicated asynchronously, so a read from a replica may return slightly stale data — it may not yet reflect a write you just made on the primary. When a read must observe a recent write (read-your-writes), pin it to the primary with quark.Sticky:

// Write goes to the primary.
_ = quark.For[User](ctx, client).Create(&u)

// A normal read may hit a replica that hasn't caught up yet.
// Sticky pins this read to the primary so it sees the write.
fresh, _ := quark.For[User](quark.Sticky(ctx), client).
Where("id", "=", u.ID).
List()

Sticky is a no-op when no replicas are configured.

See ADR-0015 for the routing model and its rationale.