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.