A migration with a batch size and a sleep

Updating four million rows in one statement holds a transaction open long enough for the replica to fall twenty minutes behind.

$lastId = 0;

do {
    $affected = DB::update(
        'UPDATE orders SET region = ? WHERE id > ? AND region IS NULL
         ORDER BY id LIMIT 2000',
        [$region, $lastId],
    );

    $lastId += 2000;
    usleep(100_000);              // 100ms
} while ($affected > 0);

The sleep is not politeness, it is what gives the replica a chance to apply — without it the batching still produces continuous write load and the lag grows anyway. The better version reads the replica lag each iteration and sleeps proportionally, which turns a guessed constant into a control loop and is about ten extra lines.