Skip to content

[6.x] Serve front-end requests from the read replica - #20640

Merged
riasvdv merged 2 commits into
6.xfrom
bugfix/read-replica-front-end
Nov 12, 2026
Merged

riasvdv merged 2 commits into
6.xfrom
bugfix/read-replica-front-end

Conversation

@riasvdv

@riasvdv riasvdv commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Description

Laravel's read/write connections mostly work in Craft already, but three things still went to the write connection (the primary). Front-end requests kept querying the primary, and a site couldn't serve pages from its replica while the primary was down.

 Cms::isInstalled()
-  DB::connection()->getPdo()            # opens the primary; 503 on every page when it's down
+  DB::connection()->getReadPdo()

 Fields / Sections / EntryTypes / ElementQuery / EloquentElement column checks
-  Schema::hasColumn() / getColumnListing()   # Laravel always introspects the write connection
+  DB::getReadSchemaBuilder()->…              # new Connection macro

 ConnectionConfig::dsn()
-  ':host='.$connection->getConfig('host')    # "Array to string conversion" when host is a list
+  ':host='.Arr::first(Arr::wrap(...))

getReadSchemaBuilder() introspects the read connection through Laravel's {name}::read connection. If the connection would read from the write connection anyway (in the console, inside a transaction, or after a write when sticky is on), it uses the normal schema builder instead. That way, a request that has just migrated never reads stale schema from a lagging replica.

Evidence

I tested a workbench in an orb with streaming replication on PostgreSQL 15 and on MySQL 8.4 (GTID, super_read_only replica). I counted the statements each server logged for every request.

Request (anonymous front end) Before After
Entry page, primary up 5–6 queries on the primary 0 on the primary, all on the replica
Entry page, primary down 503 (isInstalled()), then 500 (schema checks) 200
Search, eager loading, pagination, {% cache %}, users, relations, existing transforms; primary down 503 / 500 200, 0 queries on the primary
Control panel entry save, primary up — Writes go to the primary and replicate; reads stick to the primary after the first write

New regression tests (each fails on the old code):

CmsTest: returns true when only the read connection is reachable
  before: false   after: true
ConnectionConfigTest: builds requirement checker dsn values from read/write connection config
  before: ErrorException "Array to string conversion"   after: pgsql:host=10.0.0.1;port=5433;…
DatabaseServiceProviderTest: introspects the read connection unless the connection reads from the write connection
  before: undefined macro   after: replica columns, then primary columns after useWriteConnectionWhenReading()

Known limits that still need the primary:

  • Generating a transform for the first time (it inserts an imagetransformindex row).
  • Anything that writes.
  • Database-backed sessions, cache, and queues.
  • Legacy Yii queries (Craft::$app->db).

Craft's runtime column checks went through Laravel's schema builder, which
always introspects the write connection, and Cms::isInstalled() opened the
write PDO, so every request queried the primary and failed when it was down.

- Add a getReadSchemaBuilder() connection macro and use it for runtime column checks
- Check the read connection in Cms::isInstalled()
- Fix ConnectionConfig::dsn() for read/write configs that list hosts

Amp-Thread-ID: https://ampcode.com/threads/T-01a1d073-c4bf-74bd-9848-8353ad4c0f73
Co-authored-by: Amp <amp@ampcode.com>
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

No changed components detected in this Storybook.

resources/js — open Storybook

No changed components detected in this Storybook.

Windows can't delete a file that an open PDO still holds.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1d073-c4bf-74bd-9848-8353ad4c0f73
Co-authored-by: Amp <amp@ampcode.com>
@riasvdv
riasvdv merged commit c3887ff into 6.x Nov 12, 2026
22 checks passed
@riasvdv
riasvdv deleted the bugfix/read-replica-front-end branch November 12, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant