Master Laravel database read replicas to scale Postgres. Learn how to manage connection switching, handle replication lag, and ensure data consistency.
When my team hit a wall with database contention during a flash sale, we realized our single Postgres instance was gasping for air. We had two choices: vertical scaling, which is a temporary band-aid, or horizontal scaling. We chose the latter, implementing Laravel database read replicas to offload the heavy read traffic while keeping writes on our primary node.
The setup is surprisingly straightforward, but the operational reality—specifically dealing with the "I just saved this record, why can't I see it?" bug—is where the real engineering begins.
Laravel has built-in support for read-write splitting. You don't need a complex proxy like HAProxy or PgBouncer to start, though they help later. You configure your config/database.php to define a read array within your connection settings.
Here is how our production config looks for a standard Postgres setup:
PHP'pgsql' => [ 'driver' => 'pgsql', 'url' => env('DATABASE_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'read' => [ 'host' => [ env('DB_READ_HOST_1'), env('DB_READ_HOST_2'), ], ], 'write' => [ 'host' => [ env('DB_HOST'), ], ], 'sticky' => true, #6A9955">// ... ],
The sticky option is a lifesaver. It tells Laravel that if you’ve performed a write operation during the current request cycle, it should use the write connection for any subsequent reads. This solves the immediate consistency problem where a user updates their profile and immediately refreshes to see the old data because the replica hasn't caught up yet.
While Postgres replication is incredibly fast, it isn't instantaneous. We usually see a delay of around 50ms to 150ms in our AWS environment. That sounds negligible, but it’s an eternity if you’re redirecting users immediately after a write.
We initially tried to handle this by manually forcing a connection switch using DB::connection('pgsql')->statement(...), but that quickly turned into a maintenance nightmare. If you're struggling with similar architectural bottlenecks, you might want to look into Laravel Read-Write Splitting: Deterministic Connection Routing Guide to keep your routing logic clean.
Once you move to horizontal scaling Laravel apps, you have to accept that your system is now eventually consistent. If you have a critical path where the user must see their data immediately after a write, you have a few options:
sticky => true flag enabled in your database config.DB::connection('pgsql')->useWritePdo()->table('...')->get().We once tried to use a "global" flag to force all reads to the primary for a specific user role. It broke under load because the primary was still the bottleneck. We learned that it’s better to selectively route queries than to treat the database as a monolith.
The most common mistake I see is developers forgetting that database connection switching happens at the PDO level. If you open a transaction, Laravel automatically pins that transaction to the write connection.
However, if you're using raw queries or third-party packages that don't respect the sticky setting, you might accidentally query a replica during an active transaction. Always verify your DB::getQueryLog() in your local environment to see exactly which host is hitting which query.
If your system relies on complex event-driven updates, you might find that Laravel Transactional Outbox with Change Data Capture for Consistency is a much cleaner way to handle data synchronization than trying to force synchronous reads.
Does this setup work with migrations?
Yes, Laravel migrations always run on the write connection. You don't need to worry about schema changes hitting the replicas mid-migration.
What happens if a replica dies? Laravel’s default behavior is to try the next host in the array. If you have load balancer health checks, you should ensure your replicas are pulled from the rotation before they crash.
Is it worth the complexity? If your read-to-write ratio is 10:1 or higher, absolutely. If your database is under 20% CPU usage, you’re just adding points of failure without a performance gain.
We’re still tuning our connection pool sizes. I’m currently experimenting with fine-tuning the min_pool_size to prevent connection spikes during high-traffic events. It's a constant game of balancing performance against the overhead of maintaining these extra nodes.
Master Postgres logical decoding for real-time CDC. Learn how to stream database changes effectively to build robust, event-driven architectures today.