Read/Write Splitting &amp; Sticky Reads in Laravel | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

 Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel
======================================================================

 Learn how Laravel's database layer handles read/write splitting, when sticky reads matter, and how to layer PgBouncer or ProxySQL connection pooling without surprising your application.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 26 Jun 2026 · Updated 26 Jun 2026 · 4 min read

ShareCopy linkCopied

 ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/295/d977bd189583149245c03d6d763d9db5.png) 

  On this page +1. [Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](#readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel)
2. [Configuring a Read Replica](#configuring-a-read-replica)
3. [What sticky Actually Does](#what-codestickycode-actually-does)
4. [Forcing a Connection Explicitly](#forcing-a-connection-explicitly)
5. [Layering PgBouncer or ProxySQL](#layering-pgbouncer-or-proxysql)
6. [Transactions Always Use the Write Connection](#transactions-always-use-the-write-connection)
7. [Monitoring Replication Lag](#monitoring-replication-lag)
8. [Takeaways](#takeaways)

 Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel
---------------------------------------------------------------------

Laravel's database layer has first-class support for read/write splitting, but the defaults hide several sharp edges that only appear under production load. This article walks through the configuration mechanics, the sticky-read guarantee, and how to safely introduce a connection pooler without breaking transactions or session state.

### Configuring a Read Replica

The `read` / `write` keys in `config/database.php` let you declare separate hosts while inheriting the rest of the connection config:

```php
'mysql' => [
    'driver' => 'mysql',
    'read' => [
        'host' => [
            env('DB_READ_HOST_1', '10.0.1.11'),
            env('DB_READ_HOST_2', '10.0.1.12'),
        ],
    ],
    'write' => [
        'host' => env('DB_HOST', '10.0.1.10'),
    ],
    'sticky' => true,
    'database' => env('DB_DATABASE', 'app'),
    'username' => env('DB_USERNAME', 'app'),
    'password' => env('DB_PASSWORD'),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
],

```

Laravel randomly picks one host from the `read` array per request. All `SELECT` statements that are not inside an explicit transaction route to the read connection; everything else goes to write.

### What `sticky` Actually Does

When `sticky` is `true`, any write performed during the current request causes all subsequent reads **in the same request lifecycle** to be redirected to the write connection. This prevents the classic "I just inserted a row and immediately can't find it" bug caused by replication lag.

The flag is tracked on the `ConnectionInterface` instance held by the `DatabaseManager`. It resets between requests because the manager is resolved fresh from the container on each HTTP request — but **not** in long-lived Octane workers. Under Octane you must reset it manually or accept that a write in request N keeps reads on the primary for the remainder of that worker's life.

```php
// Octane: reset sticky state between requests
app('db')->purge(); // drops both connections and resets sticky flag

```

A lighter alternative is to call `DB::reconnect()` only when you know a write occurred, but `purge()` is safer.

### Forcing a Connection Explicitly

Sometimes you need to bypass the automatic routing — for example, reading a freshly-committed row in a background job that runs milliseconds after the HTTP response:

```php
$user = User::on('mysql::write')->find($id);

```

The `on()` method accepts the connection name. The `::write` suffix is a Laravel convention that resolves to the write PDO instance of the named connection.

### Layering PgBouncer or ProxySQL

Connection poolers sit between your app servers and the database. The key concern is **transaction-mode pooling**: PgBouncer in transaction mode reuses a server connection after each transaction, which means session-level state (prepared statements, `SET LOCAL`, advisory locks) does not survive across queries.

**Disable prepared statements** when using PgBouncer in transaction mode:

```php
// config/database.php — PostgreSQL
'pgsql' => [
    ...
    'options' => [
        PDO::ATTR_EMULATE_PREPARES => true,
    ],
],

```

For MySQL behind ProxySQL, set `wait_timeout` on the ProxySQL hostgroup to a value lower than MySQL's own `wait_timeout` to avoid "MySQL server has gone away" errors on idle connections.

### Transactions Always Use the Write Connection

Laravel wraps `DB::transaction()` and `DB::beginTransaction()` in the write connection automatically. Reads inside the transaction callback also go to write, regardless of the `sticky` flag. This is correct behaviour — you need read-your-own-writes consistency inside a transaction.

```php
DB::transaction(function () {
    $order = Order::create([...]);      // write
    $items = $order->items()->get();    // also write — correct
});

```

### Monitoring Replication Lag

No amount of sticky-read configuration helps if your replica is 30 seconds behind. Expose replication lag as a metric (via `SHOW SLAVE STATUS` or `pg_stat_replication`) and alert on it. Consider routing reads to the primary automatically when lag exceeds a threshold using a custom `Connection` resolver.

### Takeaways

- `sticky => true` prevents read-after-write anomalies within a single request but resets per request — not per Octane worker.
- Use `Model::on('mysql::write')` to force primary reads in jobs or event listeners that run close to a write.
- Disable PDO prepared statements (`ATTR_EMULATE_PREPARES`) when PgBouncer runs in transaction mode.
- Transactions always use the write connection; reads inside them do too.
- Monitor replication lag independently — application-level sticky reads cannot compensate for a lagging replica.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [database](https://www.msaied.com/public/articles?search=database)
- [performance](https://www.msaied.com/public/articles?search=performance)
- [postgresql](https://www.msaied.com/public/articles?search=postgresql)
- [mysql](https://www.msaied.com/public/articles?search=mysql)

 Frequently asked questions 
---------------------------

  Does `sticky =&gt; true` work correctly under Laravel Octane?No, not automatically. Octane reuses worker processes across requests, so the sticky flag set by a write in one request persists for the worker's lifetime. Call `app('db')-&gt;purge()` in an Octane request lifecycle hook to reset it between requests.

   Why do I get 'prepared statement does not exist' errors with PgBouncer?PgBouncer in transaction mode does not preserve session state between transactions, so named prepared statements created in one transaction are gone by the next. Set `PDO::ATTR\_EMULATE\_PREPARES =&gt; true` in your PostgreSQL connection options to use client-side emulation instead.

   Can I use different credentials for read and write connections?Yes. Any key placed inside the `read` or `write` array overrides the top-level value for that connection type. You can specify separate `username`, `password`, or `port` values per role.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleTurn PHP Attributes Into Docs With Signal](https://www.msaied.com/public/articles/turn-php-attributes-into-docs-with-signal) [Next articleLaravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware](https://www.msaied.com/public/articles/laravel-pipeline-pattern-building-custom-pipelines-beyond-middleware-1)  

   On this page
-------------

1. [Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](#readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel)
2. [Configuring a Read Replica](#configuring-a-read-replica)
3. [What sticky Actually Does](#what-codestickycode-actually-does)
4. [Forcing a Connection Explicitly](#forcing-a-connection-explicitly)
5. [Layering PgBouncer or ProxySQL](#layering-pgbouncer-or-proxysql)
6. [Transactions Always Use the Write Connection](#transactions-always-use-the-write-connection)
7. [Monitoring Replication Lag](#monitoring-replication-lag)
8. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
