A single DELETE removing two million rows holds locks for the duration, generates one enormous transaction in the binary log, and a replica applying it single-threadedly falls behind by however long it takes.
-- repeat until zero rows affected
DELETE FROM sessions
WHERE last_seen < NOW() - INTERVAL 30 DAY
LIMIT 5000;
Each batch is its own transaction, so locks are released between them and the replica applies them as they arrive. A short sleep between batches gives replication room to keep up on a busy server. The WHERE needs an index or every batch scans from the start and the job gets slower as it progresses — which is the usual reason a batched delete appears to hang after the first few thousand rows.