Double your database capacity without changing a single line of application code.
Most Laravel apps use a single database server. Every query — SELECT, INSERT, UPDATE, DELETE — hits the same machine. When traffic grows, the database becomes the bottleneck. Read/write splitting is the first step to database scaling.
For a typical Laravel app:
SELECT (reads)INSERT/UPDATE/DELETE (writes)Writes go to master. Reads go to replicas. Laravel handles the routing automatically.
| Architecture | Max Reads/sec | Max Writes/sec | Cost | Failure Tolerance |
|---|---|---|---|---|
| Single Database | 1,000 | 500 | 1 server | None (single point of failure) |
| 1 Master + 1 Replica | 2,000 (2x) | 500 | 2 servers | Replica can fail, master still works |
| 1 Master + 3 Replicas | 4,000 (4x) | 500 | 4 servers | Multiple replicas can fail |
| 1 Master + 10 Replicas | 11,000 (11x) | 500 | 11 servers | High fault tolerance |
Read capacity scales linearly with number of replicas. With 10 replicas, you can handle 10x the read traffic. Write capacity stays the same (but that's fine because writes are only 10% of traffic).
Laravel handles read/write splitting automatically. You just configure it in config/database.php.
// config/database.php
'connections' => [
'mysql' => [
// Write connection (Master)
'write' => [
'host' => env('DB_HOST_WRITE', 'master.db.example.com'),
],
// Read connections (Replicas) — Laravel randomly picks one
'read' => [
'host' => [
env('DB_HOST_READ_1', 'replica1.db.example.com'),
env('DB_HOST_READ_2', 'replica2.db.example.com'),
env('DB_HOST_READ_3', 'replica3.db.example.com'),
],
],
// Common settings (shared by both)
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'driver' => 'mysql',
],
],
# .env
DB_HOST_WRITE=master.db.example.com
DB_HOST_READ_1=replica1.db.example.com
DB_HOST_READ_2=replica2.db.example.com
DB_HOST_READ_3=replica3.db.example.com
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myuser
DB_PASSWORD=mypassword
// Your existing code — NO CHANGES NEEDED
$users = DB::table('users')->get(); // ✅ Automatically uses a replica
$user = User::find(123); // ✅ Automatically uses a replica
$user->name = 'New Name'; // ✅ Automatically uses master for writes
$user->save(); // ✅ Master
// Eloquent works the same way
User::where('active', true)->get(); // ✅ Replica
User::create([...]); // ✅ Master
// Transactions always go to master
DB::transaction(function () {
// All queries inside transaction → Master
});
MySQL replication is asynchronous. When you write to the master, there's a small delay (milliseconds to seconds) before the replica receives the change.
Force the user to always read from master for a few seconds after writing. Laravel doesn't do this automatically.
For operations where consistency is critical, explicitly use the master connection:
// Force a read to use master (not replica)
$post = DB::connection('mysql_write')
->table('posts')
->where('user_id', $userId)
->get();
// Or in Eloquent
$post = DB::connection('mysql_write')
->table('posts')
->find($postId);
For most Laravel apps (forums, social media, blogs), a 50ms delay is acceptable. Users won't notice. Only use master reads for critical operations like payments.
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_do_db = myapp_database
binlog_format = ROW
[mysqld]
server-id = 2
relay-log = /var/log/mysql/mysql-relay-bin.log
log_bin = /var/log/mysql/mysql-bin.log
read_only = 1 # Important! Prevents writes to replica
CREATE USER 'replication'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'replication'@'%';
CHANGE MASTER TO
MASTER_HOST='master.db.example.com',
MASTER_USER='replication',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0;
START SLAVE;
AWS RDS, DigitalOcean Managed Databases, and Laravel Forge have one-click read replica creation. No manual configuration needed.
SHOW SLAVE STATUS\G
-- Look for:
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- Seconds_Behind_Master: 0 (or small number)
// routes/health.php
Route::get('/health/database', function () {
try {
$replicas = config('database.connections.mysql.read.host', []);
$results = [];
foreach ($replicas as $replica) {
$start = microtime(true);
DB::connection('mysql')->select('SELECT 1');
$time = (microtime(true) - $start) * 1000;
$results[$replica] = ['status' => 'ok', 'latency_ms' => $time];
}
return response()->json($results);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
});
| Concept | Single Database | Master + Replicas |
|---|---|---|
| Read capacity | 1x (bottleneck) | Nx (number of replicas) |
| Write capacity | 1x | 1x (master only) |
| Read/write contention | High (reads block writes) | None (separate servers) |
| Fault tolerance | None (single point of failure) | High (replicas can fail) |
| Laravel code changes | None | None (just config) |
| Best for | Small apps (< 1000 req/sec) | Medium to large apps |
Topic 11: Database Partitioning (Vertical & Horizontal) — When even replicas aren't enough. How to split massive tables across multiple servers. Sharding by user_id or date.