Laravel database sharding allows you to scale beyond single-instance limits. Learn how to implement horizontal partitioning using custom connection resolvers.
When your primary database instance starts hitting IOPS limits during peak traffic, vertical scaling stops being an option. We faced this last year while supporting a multi-tenant system that grew to roughly 40 million rows across our core activity tables; the indexes were bloated, and query times started creeping past 300ms. If you’ve reached this point, you’re ready to look at Database Sharding for High-Concurrency: A Practical Scaling Guide to understand the broader strategy before writing a single line of code.
Horizontal partitioning isn't about just splitting tables; it’s about deterministic routing. You need a way to look at a tenant_id or user_id and know exactly which database connection to use. We initially tried a round-robin approach, but it turned into a nightmare for reporting and cross-shard joins. We switched to a hash-based deterministic routing strategy.
By mapping a shard key to a specific connection name in your config/database.php, you keep the logic predictable. Here is how we implemented a custom connection resolver in Laravel 10.
First, define your shards in the configuration:
PHP'connections' => [ 'shard_0' => [ #6A9955">/* ... */ ], 'shard_1' => [ #6A9955">/* ... */ ], ],
To make this feel native, I prefer extending the model's connection logic rather than manually setting the connection every time. We created a Shardable trait that hooks into the model's getConnectionName method.
PHPtrait Shardable { public function getConnectionName() { $shardId = $this->resolveShardId(); return "shard_{$shardId}"; } protected function resolveShardId() { #6A9955">// Deterministic hash: user_id % total_shards return (int) $this->shard_key % 2; } }
This works great for individual model instances, but it fails when you try to perform bulk queries or migrations. You’ll need to iterate through your registered shards explicitly. If you're struggling with complex filtering logic, you might want to review Mastering Laravel Eloquent Scopes: Writing Reusable Query Constraints to ensure your constraints remain clean when applied across different connections.
We learned the hard way that horizontal partitioning introduces significant complexity in global queries. If you need to fetch "all active users" across the entire system, you can’t just run one query. You have to aggregate results from every shard.
We first tried using standard UNION ALL queries, but the latency hit was unacceptable because we were waiting for the slowest shard to respond. We ended up using a Parallel job execution approach to query shards concurrently.
PHP$shards = ['shard_0', 'shard_1']; $results = collect($shards)->map(function ($shard) { return DB::connection($shard)->table('users')->where('active', 1)->get(); })->flatten();
This is fine for small datasets, but for high-concurrency needs, you should consider the raw performance gains discussed in Laravel Query Builder: Build Complex Database Queries Without Eloquent. Eloquent’s hydration overhead becomes a bottleneck when you’re pulling thousands of records from four different connections at once.
Database scalability isn't just about splitting the data; it's about connection management. PHP-FPM processes will quickly exhaust your connection limits if every request opens a new connection to every shard.
Ensure your database.php config uses persistent => true where applicable, or use a tool like ProxySQL to multiplex connections before they reach your database. We found that without a proxy, our database server was spending more time performing the TCP handshake than actually executing the SQL.
Looking back, we hardcoded the shard logic too deeply into our models. If I were starting over, I’d use a dedicated ShardManager service that acts as a singleton. This would allow us to swap the routing logic—for example, moving from a modulo-based hash to a lookup table—without touching fifty different Eloquent models.
I’m still not entirely happy with how we handle migrations across shards. Right now, we run them sequentially using a custom Artisan command. It works, but it's slow. If one shard fails, you’re left with a partially migrated system, which is a production nightmare. We’re currently exploring atomic migration strategies, but that’s a topic for another deep dive.
Horizontal partitioning is a powerful tool, but it's a massive shift in how your application handles data. Don't reach for it until you've exhausted indexing, caching, and read-replicas. When you do take the leap, keep your routing logic deterministic and your connection management centralized.
Laravel database replication helps scale globally. Learn how to manage read-replicas and solve data consistency challenges in distributed systems effectively.