Scaling Databases & Data Consistency · Change Without Downtime and Eventual Consistency

Safe Backfills and Large Data Updates

Update 50 million rows without locking tables, lagging replicas or filling the disk.

Scenario

Example app: after adding users.full_name, the team must copy name into it for 50 million rows. Someone runs one UPDATE in a console on a Friday afternoon.

What you see in production

The single UPDATE ran for 70 minutes in one transaction: replicas fell 40 minutes behind, the table doubled in size (every row got a new version), autovacuum couldn't keep up, and rows locked by the update blocked user profile saves. Cancelling it rolled everything back — another 50 minutes.

The naive code

UPDATE users SET full_name = name WHERE full_name IS NULL;    -- 50M rows, one transaction

Reproduce it

A batched backfill: walk the primary key in ranges, commit each batch, and pause when replica lag rises:

Batched, throttled, resumable backfillTry it yourself
TOTAL, BATCH, LAG_LIMIT = 50_000, 10_000, 2.0
replica_lag = [0.3, 0.8, 2.6, 1.1, 0.4, 0.2]      # sampled lag (s) as batches run
checkpoint = 0                                    # last processed id, persisted for resume
batch_no = 0
while checkpoint < TOTAL:
    lag = replica_lag[batch_no % len(replica_lag)]
    batch_no += 1
    if lag > LAG_LIMIT:
        print(f"replica lag {lag}s > {LAG_LIMIT}s -> sleep and retry")
        continue
    upper = min(checkpoint + BATCH, TOTAL)
    # UPDATE users SET full_name = name WHERE id > :checkpoint AND id <= :upper AND full_name IS NULL
    print(f"batch ids ({checkpoint}, {upper}] committed, lag {lag}s")
    checkpoint = upper
print("done; safe to re-run: WHERE full_name IS NULL makes it idempotent")

Output

batch ids (0, 10000] committed, lag 0.3s
batch ids (10000, 20000] committed, lag 0.8s
replica lag 2.6s > 2.0s -> sleep and retry
batch ids (20000, 30000] committed, lag 1.1s
batch ids (30000, 40000] committed, lag 0.4s
batch ids (40000, 50000] committed, lag 0.2s
done; safe to re-run: WHERE full_name IS NULL makes it idempotent

Root cause

One huge transaction holds locks for its whole duration, generates a massive burst of WAL/binlog that replicas must replay, creates millions of dead row versions at once, and can only be undone by an equally long rollback. Row-by-row ORM loops are the opposite extreme — millions of round trips.

The fix

Backfill in small, committed, idempotent batches keyed by primary key ranges, with throttling and a checkpoint:

UPDATE users SET full_name = name
WHERE id > $1 AND id <= $1 + 10000 AND full_name IS NULL;   -- uses the PK index; idempotent

Trade-offs

  • Batches mean the backfill takes longer in wall-clock time — run it as a background job with progress reporting, not in a migration that blocks deploys.
  • Choose batch size by measuring: each batch should take well under a second.
  • Concurrent writes during the backfill must also populate the new column (dual write), or the backfill will miss rows changed after it passed them.

Detect it in production

  • Replica lag, WAL generation rate and dead tuples during backfills.
  • Progress metrics (rows remaining) and a dashboard showing the checkpoint.

Key points

  • Never update millions of rows in one transaction.
  • Batch by primary key, commit each batch, make it idempotent and resumable.
  • Throttle on replica lag and run backfills as monitored background jobs.

Practice questions

Why is a single UPDATE of 50 million rows dangerous in production?

Answer

It holds locks for its whole duration, generates a huge WAL burst that lags replicas, creates millions of dead tuples at once, and a cancel triggers an equally long rollback.

What makes a backfill batch safe to re-run?

Answer

A condition that skips already-processed rows (e.g. AND full_name IS NULL) plus a persisted checkpoint, so a crash or restart continues without double work or errors.

Why should the backfill pause when replica lag rises?

Answer

Replicas replay every batch; if they fall behind, reads served from them become stale and failover risk grows. Throttling keeps lag within your budget.

Open in the app to mark lessons complete, take notes and track your progress.