Killable at any instant
Every batch commits its work, its errors and its cursor together. A SIGKILL mid-batch costs one batch, and the resume lands on the same end state as an uninterrupted run.
The invariant
Safe, resumable, one-off data changes over large tables โ keyset paginated, crash-proof, and testable.
composer require kstmostofa/laravel-backfill
php artisan migrateclass BackfillUserSlugs extends Backfill
{
public int $batchSize = 1000;
public function collection(): Builder
{
return User::query()->whereNull('slug');
}
public function process($record): void
{
$record->update(['slug' => Str::slug($record->name)]);
}
}php artisan backfill:run user-slugs --dry-run # scope, index check, real diffs, zero writes
php artisan backfill:run user-slugs # resumable, killable, throttledThe promise that you can kill a backfill at any moment is worth exactly as much as the test behind it. So the test does not simulate a crash: it forks, and the child sends itself a real SIGKILL from inside a batch transaction โ uncatchable, no destructors, no shutdown handlers. It then asserts the resumed run reaches byte-for-byte the same end state as an uninterrupted control run, with every row processed exactly once.
PASS Tests\Chaos\HardKillTest
โ it resumes after a SIGKILL to the identical end state 1.19s
โ it leaves no work half-applied when killed inside a batch 1.11s
Tests: 245 passed (554 assertions)Green on SQLite, MySQL 8.4 and PostgreSQL 18, with the chaos test running for real on each.