Zero-Downtime and High Volumes: How to Migrate a 10-Million Row Table Without Making Your Host Sweat
How to run zero-downtime database migrations on 10 million rows in Laravel without making your host sweat or waking up your Ops on a Friday night.

We’ve all experienced that moment of profound isolation. It's Friday at 5:58 PM, and the product team walks into the open space with a big smirk: "Hey, we just need to change the user transformation format on the main table, it'll take two seconds right?"
You've probably dreamed of having Goku's power in Dragon Ball, able to unlock a new transformation with a single scream and obliterate everything in your path. Except that in real-world backend development, trying to flip everything overnight without a transition phase is the fastest way to hit a brick wall. By the time you transfer everyone from one level to the next without warning, your fighters freeze up, the queue explodes, and your host sends a polite email asking if you're mining crypto on their infrastructure.
The Super Saiyan Analogy (That Went Terribly Wrong)
To understand what happens in your database, picture the scene. Your users are fighters in the middle of a brawl, locked into a very specific power version. Everything is running smoothly, punches are flying everywhere.
Suddenly, you decide to change everything in one sudden move. Instead of rolling out the new transformation in the background while they keep fighting, you freeze time, trap everyone in a massive locking bubble, and demand they drop their current energy level to load the new version right in the middle of a clash.
The result? The server cries for help, the CPU ignites, and your users are left staring at a nice 504 gateway error. In production, it's exacty the same thing: an exclusive lock can turn a schema operation into a tollgate on the affected queries, leaving them blocked or severely slowed down.
Technical Pitfalls (Or How to Avoid Making Your Ops Cry)
Before diving in headfirst, let's do a quick audit of the myths and legends surrounding reckless migrations.
1. The Myth of the Magical ALTER TABLE
Saying an ALTER TABLE blocks everything in a binary way is jumping to conclusions.
- The reality: Modern operations (like adding a column compatible with ALGORITHM=INSTANT in MySQL) quietly limit themselves to modifying metadata without rebuilding the entire table.
- The trap: Believing this is a universal rule. Changing a data type or adding a complex constraint on a 10-million row table will indeed force the RDBMS to sweat and throw heavy locks depending on your engine and version. It's an operation to be handled with surgical precision.
2. The Trap of Data Racing and Dual Writing
During the transition, your application needs to write to both the old and new formats simultaneously (dual writing). The golden rule: first deploy application code capable of maintaining both formats consistently, then run the backfill to complete the history. The backfill must be designed not to overwrite a recent value with an outdated read.
Clean Implementation in Laravel: The Surgical Method
Let's see what a robust Expand & Contract pattern looks like in Laravel, keeping in mind that this is an introductory guide to illustrate the pedagogical principle, not a raw production runbook for gargantuan tables.
Step 1: Expand (Add the Structure Cleanly)
We start by creating the column in an isolated manner, avoiding complex physical complications:
PHP
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('power_tier_v2')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('power_tier_v2');
});
}
};
Step 2: Intelligent Batch Backfill
To limit pressure on the database, we process data in batches (chunkById). Batch size and execution cadence must be adjusted based on real load: chunking limits the size of operations, but doesn't guarantee a completely painless migration on its own.
The code below illustrates only the backfill. Dual writing belongs to the previous deployment phase: it must be set up and verified before launching this command.
PHP
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class MigrateUserPowerTier extends Command
{
protected $signature = 'db:migrate-power-tier';
protected $description = 'Migrates historical data using secure batches';
public function handle(): void
{
$this->info('Starting smooth backfill...');
// Note: chunkById(500) is an example. Monitor batch duration,
// I/O, replication, and actual worker load!
User::whereNull('power_tier_v2')->chunkById(500, function ($users) {
foreach ($users as $user) {
$newTier = $user->power_level > 9000 ? 'SUPER_SAIYAN' : 'BASE';
DB::table('users')
->where('id', $user->id)
->whereNull('power_tier_v2')
->update(['power_tier_v2' => $newTier]);
}
usleep(100000); // Take a breather to let replication catch up
});
$this->info('Backfill complete. Remember to check for residual null values!');
}
}
Step 3: The Contract (Cleaning Up at the Right Time)
We don't drop the old column on a whim after three days. We drop it only when metrics and checks confirm that all identified consumers have migrated and verifications are conclusive.
Watch out for DDL: Even when your code has stopped using the old column, make sure the drop operation itself won't trigger a critical blocking lock on your RDBMS.
Final Thoughts
Managing databases in enterprise environments is a bit like trying to change an airplane engine mid-flight: it requires methodology, a touch of pragmatism, and above all, knowing how to say no to last-minute demands driven by impatience. If you want to avoid spending your Friday evening sweating over a production server, protect your infrastructure with proper methodology. And if you're looking for a freelance engineer who knows how to set real boundaries without corporate fluff... you know where to find me.
Peter K
Senior Full-Stack Engineer • Freelance France