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

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

Menu
----

Close 

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

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

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Read/Write Splitting and Sticky Reads in Laravel: A Production Guide

 Read/Write Splitting and Sticky Reads in Laravel: A Production Guide
=====================================================================

 Configure Laravel's read/write connection splitting correctly, avoid stale-read bugs with sticky connections, and tune connection pooling for high-throughput apps — without reaching for a third-party package.

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

ShareCopy linkCopied

 ![Read/Write Splitting and Sticky Reads in Laravel: A Production Guide](https://cdn.msaied.com/757/f990951a1a0e14b4fece312bce644c29.png) 

  On this page +1. [Why Read/Write Splitting Matters](#why-readwrite-splitting-matters)
2. [The Basics: Configuring a Read Replica](#the-basics-configuring-a-read-replica)
3. [The sticky Option: What It Actually Does](#the-codestickycode-option-what-it-actually-does)
4. [Forcing a Connection Explicitly](#forcing-a-connection-explicitly)
5. [Connection Pooling Considerations](#connection-pooling-considerations)
6. [Monitoring Which Connection Is Used](#monitoring-which-connection-is-used)
7. [Key Takeaways](#key-takeaways)

 Why Read/Write Splitting Matters
--------------------------------

Once your application crosses a few hundred requests per second, a single database node becomes the bottleneck. Adding a read replica is the standard first move, but Laravel's built-in support for read/write splitting has several sharp edges that can silently serve stale data or exhaust your replica's connection pool.

This article covers the mechanics, the pitfalls, and the production-safe patterns.

---

The Basics: Configuring a Read Replica
--------------------------------------

```php
// config/database.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_WRITE_HOST', '10.0.1.10'),
    ],
    'sticky' => true,
    'database' => env('DB_DATABASE', 'app'),
    'username' => env('DB_USERNAME'),
    'password' => env('DB_PASSWORD'),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
],

```

Laravel randomly selects one host from the `read.host` array per request. All `SELECT` queries go to the read connection; `INSERT`, `UPDATE`, `DELETE`, and DDL go to the write connection.

---

The `sticky` Option: What It Actually Does
------------------------------------------

The `sticky` flag is the most misunderstood setting. When `true`, Laravel records whether the write connection was used **during the current request**. If it was, all subsequent reads in that same request are also routed to the write connection.

This prevents the classic stale-read bug:

```php
// Without sticky=true this can return the old value
$user = User::create(['email' => 'new@example.com']);
$found = User::where('email', 'new@example.com')->first(); // reads from replica!

```

With `sticky => true`, the second query hits the primary because a write already occurred in this request lifecycle.

**Caveat:** Sticky reads only apply within a single PHP process/request. Queue jobs, scheduled commands, and separate HTTP requests have no memory of prior writes. For those cases you need explicit connection hints.

---

Forcing a Connection Explicitly
-------------------------------

For queue jobs that process data immediately after a write, force the primary:

```php
// In a job's handle() method
public function handle(): void
{
    // Always read from primary in this job
    $order = Order::on('mysql::write')->find($this->orderId);

    // Or use the DB facade
    $result = DB::connection('mysql')->select(
        'SELECT * FROM orders WHERE id = ?',
        [$this->orderId]
    );
}

```

Alternatively, wrap the critical section:

```php
DB::transaction(function () use ($orderId) {
    // Inside a transaction, Laravel always uses the write connection
    $order = Order::lockForUpdate()->find($orderId);
    $order->update(['status' => 'processed']);
});

```

Transactions are always routed to the write connection — this is a safe, idiomatic way to guarantee consistency.

---

Connection Pooling Considerations
---------------------------------

Laravel does not maintain persistent connection pools itself; each PHP-FPM worker opens its own connection. With 50 workers × 3 replicas, you can easily exhaust `max_connections`.

Practical mitigations:

- **PgBouncer (PostgreSQL) / ProxySQL (MySQL):** Place a pooler in front of your replicas. Laravel connects to the pooler; the pooler multiplexes to the DB.
- **`options.PDO::ATTR_PERSISTENT`:** Persistent PDO connections reuse the socket across FPM requests. Use with care — they can hold locks and transactions across requests if not cleaned up.
- **`pool_size` in Octane:** Under Swoole/RoadRunner, connections persist across requests. Explicitly close or reset connections in the `RequestHandled` event if you use Octane.

```php
// Octane-safe connection reset
Event::listen(RequestHandled::class, function () {
    DB::purge('mysql'); // drops and re-opens on next query
});

```

---

Monitoring Which Connection Is Used
-----------------------------------

During development, log the connection name alongside slow queries:

```php
DB::listen(function ($query) {
    if ($query->time > 100) {
        Log::warning('Slow query', [
            'connection' => $query->connectionName,
            'sql' => $query->sql,
            'time_ms' => $query->time,
        ]);
    }
});

```

In production, Telescope's query watcher surfaces the connection name in the UI — invaluable for confirming that analytics queries are actually hitting replicas.

---

Key Takeaways
-------------

- `sticky => true` prevents stale reads within a single request but has no effect across jobs or separate processes.
- Use `DB::transaction()` or `Model::on('mysql::write')` to guarantee primary reads in async contexts.
- Route long-running analytics queries explicitly to a replica with `Model::on('mysql::read')`.
- Use PgBouncer or ProxySQL to avoid exhausting `max_connections` at scale.
- Under Laravel Octane, purge connections on `RequestHandled` to prevent cross-request state leakage.

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

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

  Does `sticky =&gt; true` affect queue jobs?No. The sticky flag only tracks writes within the current PHP process lifecycle (a single HTTP request). Queue workers are separate processes with no memory of prior writes, so you must explicitly use the write connection or wrap logic in a transaction.

   How do I send a specific Eloquent query to the read replica?Use `Model::on('mysql::read')-&gt;where(...)-&gt;get()` or `DB::connection('mysql')-&gt;select(...)`. Laravel resolves `mysql::read` and `mysql::write` as virtual connection names that map to the configured read/write hosts.

   Can I use read/write splitting with Laravel Octane?Yes, but you must reset connections between requests. Call `DB::purge('mysql')` in a listener for the `RequestHandled` event, or use `DB::reconnect()`. Otherwise a worker may reuse a connection that was left in a transaction or pointing to the wrong host.

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

[Mohamed Said](https://msaied.com#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://msaied.com#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 articleEloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination](https://msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination)  

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

1. [Why Read/Write Splitting Matters](#why-readwrite-splitting-matters)
2. [The Basics: Configuring a Read Replica](#the-basics-configuring-a-read-replica)
3. [The sticky Option: What It Actually Does](#the-codestickycode-option-what-it-actually-does)
4. [Forcing a Connection Explicitly](#forcing-a-connection-explicitly)
5. [Connection Pooling Considerations](#connection-pooling-considerations)
6. [Monitoring Which Connection Is Used](#monitoring-which-connection-is-used)
7. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

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

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

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

 [ ![](https://cdn.msaied.com/756/69efdf9ac9ec1e1f61378055f1cd6e92.png)  · 4 min read### Eloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination

9 Oct 2026 ](https://msaied.com/articles/eloquent-at-scale-chunked-iteration-lazy-collections-and-cursor-pagination) [ ![](https://cdn.msaied.com/755/2afbdc0a82de6bd61d3739c09554dda0.png)  · 4 min read### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Mess

8 Oct 2026 ](https://msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-mess) [ ![](https://cdn.msaied.com/754/1ab77f05a0bd841971617d39adabd22e.png) Laravel · 3 min read### Laravel Fake Assertions Now Accept Property Arrays in Laravel 13.35

7 Oct 2026 ](https://msaied.com/articles/laravel-fake-assertions-now-accept-property-arrays-in-laravel-1335) 

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

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

 [Discuss your project ↗](https://msaied.com#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://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/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://msaied.com/sitemap.xml)
