allnetru / laravel-sharding
Laravel database sharding toolkit
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/pagination: ^12.0|^13.0
- illuminate/redis: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.87
- larastan/larastan: ^3.0
- orchestra/testbench: ^10.0|^11.0
- phpstan/extension-installer: ^1.3.1
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.6.1
- v0.6.0
- v0.5.7
- v0.5.6
- v0.5.5
- v0.5.4
- v0.5.3
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.1
- v0.4.0
- v0.3.14
- v0.3.13
- v0.3.12
- v0.3.11
- v0.3.10
- v0.3.9
- v0.3.8
- v0.3.7
- v0.3.6
- v0.3.5
- v0.3.4
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.1
- v0.2.0
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- dev-fix/zero-is-a-shard-key
- dev-chore/update-changelog-v0.6.1
- dev-every-bounded-read-refuses-the-same
- dev-chore/update-changelog-v0.6.0
- dev-rows-move-between-shards
- dev-chore/update-changelog-v0.5.7
- dev-an-aggregate-may-sit-deeper
- dev-chore/update-changelog-v0.5.6
- dev-an-aggregate-takes-no-ordering
- dev-chore/update-changelog-v0.5.5
- dev-query-keeps-its-own-signature
- dev-the-alias-is-the-first-source
- dev-feature/raw-statements-by-shard-key
This package is auto-updated.
Last update: 2026-09-21 23:45:11 UTC
README
Laravel Sharding is a toolkit for distributing data across multiple databases while keeping a familiar Eloquent workflow. The package powers production applications and provides pluggable strategies so each table can select the most appropriate sharding approach. Shards can run on MySQL, PostgreSQL, SQL Server, or SQLite as long as the connections are available to Laravel.
Requirements
- PHP ^8.2
- Laravel 12.x or any framework using Illuminate 12 components
- MySQL, PostgreSQL, SQL Server, or SQLite for shard connections
- Redis (optional) when using the Redis-backed strategy
Installation
Require the package via Composer:
composer require allnetru/laravel-sharding
The service provider is auto-discovered. Publish the configuration and optional migrations with:
php artisan vendor:publish --tag=laravel-sharding-config php artisan vendor:publish --tag=laravel-sharding-migrations
Run the migrations to create metadata tables used by the built-in strategies:
php artisan migrate
Configuration
- Define shard connections through the
DB_SHARDSenvironment variable. Each entry follows the formatname:host:port:databaseand multiple shards are separated by semicolons. - When preparing to migrate or remove shards, list them in
DB_SHARD_MIGRATIONS. New writes are skipped for shards in this list until you finish rebalancing. - Review
config/sharding.phpto map tables to strategies, configure shard groups, and choose ID generators. Every shard-aware model should use the providedShardabletrait.
A minimal example stitches these pieces together:
# .env DB_SHARDS="shard-1:10.0.0.10:3306:app_shard_1;shard-2:10.0.0.11:3306:app_shard_2;shard-archive:10.0.0.12:3306:app_archive" DB_SHARD_MIGRATIONS="shard-legacy;shard-archive"
// config/sharding.php return [ 'default' => 'hash', 'strategies' => [ 'hash' => Allnetru\Sharding\Strategies\HashStrategy::class, 'redis' => Allnetru\Sharding\Strategies\RedisStrategy::class, 'range' => Allnetru\Sharding\Strategies\RangeStrategy::class, 'db_range' => Allnetru\Sharding\Strategies\DbRangeStrategy::class, 'db_hash_range' => Allnetru\Sharding\Strategies\DbHashRangeStrategy::class, ], 'id_generator' => [ 'default' => 'snowflake', 'strategies' => [ 'snowflake' => Allnetru\Sharding\IdGenerators\SnowflakeStrategy::class, 'sequence' => Allnetru\Sharding\IdGenerators\TableSequenceStrategy::class, ], 'sequence_table' => 'shard_sequences', // 'meta_connection' => 'pgsql', // use any connection name configured in database.php ], 'connections' => [ 'shard-1' => ['weight' => 2], 'shard-2' => ['weight' => 1], // 'shard-archive' => ['weight' => 1, 'replica' => true], ], 'replica_count' => 1, 'tables' => [ // 'users' => [ // 'strategy' => 'redis', // 'redis_connection' => 'shards', // 'redis_prefix' => 'user_shard:', // 'group' => 'user_data', // ], 'users' => [ 'strategy' => 'db_hash_range', 'slot_size' => 250_000, 'connections' => [ 'shard-1' => ['weight' => 2], 'shard-2' => ['weight' => 1], ], 'meta_connection' => 'mysql', 'group' => 'user_data', ], 'profiles' => [ // inherits the shard selected for the `users` table 'group' => 'user_data', // 'id_generator' => 'sequence', ], 'orders' => [ 'strategy' => 'db_range', 'connections' => [ 'shard-1' => ['weight' => 2], 'shard-2' => ['weight' => 1], ], 'range_size' => 50_000, 'meta_connection' => 'mysql', // 'range_table' => 'order_ranges', ], // 'payments' => [ // 'strategy' => 'range', // 'ranges' => [ // ['start' => 1, 'end' => 1_000_000, 'connection' => 'shard-1'], // ['start' => 1_000_001, 'end' => null, 'connection' => 'shard-2'], // ], // ], ], 'groups' => [ 'user_data' => ['users', 'profiles', 'orders'], // 'billing' => ['payments', 'refunds'], ], ];
Update config/database.php to merge the generated shard connections with your base definitions. The examples below use MySQL, but you can swap in any of Laravel's supported drivers:
// config/database.php (excerpt) use Allnetru\Sharding\Support\Config\Shards; return [ 'default' => env('DB_CONNECTION', 'mysql'), 'connections' => array_merge([ 'mysql' => [ 'driver' => 'mysql', 'url' => env('DB_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), // ... keep the rest of your base connection definition ], // other non-sharded connections... ], Shards::databaseConnections(env('DB_SHARDS', ''))), // ... ];
Note Passing the
DB_SHARDSstring ensures shard definitions are available while configuration files are still being evaluated. In other contexts you may callShards::databaseConnections()without arguments and it will read theDB_SHARDSenvironment variable directly.
A full walkthrough is available in docs/en/sharding.md.
Usage
Creating sharded tables
Create tables with an unsigned big integer primary key and the is_replica flag to track replicated rows:
Schema::create('items', function (Blueprint $table) { $table->unsignedBigInteger('id')->primary(); $table->boolean('is_replica')->default(false); $table->timestamps(); });
Then register the table inside config/sharding.php, select a strategy (hash, redis, range, db_range, or db_hash_range), and list the shard connections the table can use.
ID generation
The default snowflake generator creates sortable 64-bit identifiers laid out
as 41 bits timestamp | 10 bits worker | 12 bits sequence. You can switch the
global default or override per table to use a database-backed sequence
generator or any other configured strategy.
Give every process that mints ids its own worker_id. The sequence counter
is per process, so two processes sharing a worker id share a counter space and
can produce the same identifier. On one shard that is a primary key violation;
across shards it is a silent duplicate that later breaks find() and
rebalancing.
# distinct per container, per app server, per queue worker host SHARDING_WORKER_ID=1
Valid range is 0 to 1023. When the option is unset the worker id is derived from the hostname and the process id, which is adequate for a single node but is a fallback, not a guarantee.
The epoch defaults to 2020-01-01 and 41 bits cover about 69 years from it.
Changing SHARDING_EPOCH_MS on a populated database reorders identifiers, so
treat it as fixed once data exists.
Grouping related tables
Group tables so records that belong together end up on the same shard:
'groups' => [ 'user_data' => ['users', 'organizations', 'billing', 'transactions'], ],
When models belong to a group they reuse the shard selected for the group's
primary table (for example, users).
Colocation needs three things together, and missing the third is the common mistake because nothing fails loudly:
- the entry in
groups, with the owning table listed first; 'group' => 'user_data'in the table's own configuration;protected string $shardKey = 'user_id';on the child model.
Without the third the child is hashed by its own primary key and lands on an arbitrary shard, so the parent and its children end up apart while everything appears to work.
A child whose shard key is not its primary key must not use an auto-incrementing
primary key either: two shards would hand out the same sequence values. Declare
public $incrementing = false; and the package generates the key for you.
Working with data
Models using the Shardable trait behave like standard Eloquent models:
$user = User::find(15); $partners = Organization::where('status', OrganizationStatus::partner) ->paginate(50);
Insertions also resolve the target shard automatically. If you omit the primary key the configured ID generator assigns one before the record is saved.
Moving what a key owns
A key decides which shard a row lives on, so changing it is not an update:
the new key may name a different database, and update() refuses to change a
shard key for that reason. When the move is genuinely what is wanted — a
settlement bought by the company that runs it, an account merged into another
— ShardMover does it:
app(ShardMover::class)->move( new Parcel(), ['parcels', 'buildings', 'tasks'], from: $oldTenantId, to: $newTenantId, filter: fn ($query) => $query->where('settlement_id', $settlement->getKey()), ); // a group can key its tables differently, and name their rows differently: // give the column, or the model, per table app(ShardMover::class)->move(new User(), [ 'user_roles' => UserRole::class, 'user_permissions' => UserPermission::class, ], from: $userId, to: $keptUserId);
Within one shard it is an update. Across two it is a chunked copy followed by a delete, in that order: a row present twice for an instant is recoverable, a row absent from both is not. The filter narrows what moves, so a tenant can move one settlement rather than everything it owns.
The owning table is left out of that second example on purpose: moving
users from one identifier to another would rewrite the row's own primary key
onto a row that already exists. What moves is what belongs to the key, and
the row the key names is the application's to deal with.
It moves rows and nothing else. Whether the move is allowed is the application's to decide, and so is what happens if it is interrupted: there is no transaction across connections, so a failure part-way leaves rows copied but not yet deleted. Run it where a retry is safe — a queued job whose work is idempotent — or take the settlement offline for the duration.
Transactions
A transaction lives on one connection, and DB::transaction() opens it on the
default one — which on a sharded schema wraps nothing the callback touches:
the writes inside go to the shard their key names and commit one by one
regardless. Open it where the rows are instead:
$parcel->transaction(fn () => ...); Parcel::query()->where('tenant_id', $tenantId)->transaction(fn () => ...);
Both need the shard named: the row carries its key, the builder pins one with
where(shardKey, …) or with onShardConnection(). Two values of the key are
two shards and a transaction cannot span them, so that is refused with
UnsupportedCrossShardQuery rather than guessing which one to open.
Running under Swoole
When the PHP process is executed inside a Swoole coroutine context (for example,
Laravel Octane with the Swoole engine), shard fan-out queries are dispatched
concurrently. The package detects the coroutine runtime automatically and uses
channels to aggregate results without blocking on each individual shard. When a
request is not already inside a coroutine, the dispatcher boots a
Swoole\Coroutine::run() scheduler so the queries still run in parallel. No
additional configuration is required.
Custom coroutine drivers
The default behaviour can be overridden from config/sharding.php. The
coroutines section accepts any class or closure that returns an implementation
of Allnetru\Sharding\Support\Coroutine\CoroutineDriver, allowing you to disable
coroutines entirely or integrate with an alternative runtime:
'coroutines' => [ 'default' => env('SHARDING_COROUTINE_DRIVER', 'swoole'), 'drivers' => [ 'swoole' => Allnetru\Sharding\Support\Coroutine\Drivers\SwooleCoroutineDriver::class, 'sync' => Allnetru\Sharding\Support\Coroutine\Drivers\SyncCoroutineDriver::class, 'amphp' => App\Sharding\AmpCoroutineDriver::class, ], ],
Point the default driver to sync (or set SHARDING_COROUTINE_DRIVER=sync) to
keep fan-out queries synchronous. Custom drivers may be resolved through the
Laravel container, so you can bind them as singletons or expose factory closures
for more advanced scenarios.
Console tooling
Use the bundled Artisan commands to inspect and maintain shards:
php artisan shards:distribute {model} [{model} ...] [--dry-run]– put existing rows on the shard their own key names, in chunks. One model per table: the tables of a colocation group share a key but not the column it lives in (users.idanduser_roles.user_idare the same key under two names), and only the model knows its own.--dry-runcounts what is misplaced without moving anything, which is what to run before an upgrade.php artisan shards:rebalance {model} [{model} ...]– move the rows of a colocation group between shards with optional--from,--to,--start, and--endfilters. Model classes rather than table names, one per table of the group, for the same two reasonsshards:distributetakes them: a table name cannot find the model outsideApp\Models, and only the model knows the column its own shard key lives in. Every populated table of the group has to be named — the routing being handed over belongs to the group, so moving one table would strand the others behind it — and an empty sibling may be left out.--startand--endbound the shard key; a range strategy needs both of them with--to, anddb_hash_rangerefuses either of them with--to, because its slots are hashes of the key rather than ranges of it and a partly moved slot cannot be redirected.php artisan shards:migrate– run shard-specific migrations across every configured connection.
Testing
Clone the repository and install dependencies before running the test suite:
composer install
composer test
Contributing
Please review the CONTRIBUTING.md guide for details about our workflow, coding standards, and security policy.
Security
If you discover a security vulnerability, please follow the disclosure process described in CONTRIBUTING.md.
License
Laravel Sharding is open-sourced software licensed under the MIT license.