skip to content

You own a busy transactional database that also serves hour-long analytical reports and feeds a downstream change-data-capture consumer. How would you set a policy for how long old row versions are retained, and keep cleanup from either falling behind or starving the workload?

level: principalimportance: nice to knowfreq 26%

answer

  1. retention = one global dial, set by the furthest-behind consumer
  2. choose: aborted readers vs unbounded bloat
  3. enforce with timeouts + slot caps, not convention
  4. cleanup rate must exceed dead-version rate
  5. partition drop = zero dead versions

basics

~20 s

Pick a maximum history depth the platform will fund, enforce it mechanically with transaction and slot limits, and decide up front which failure you prefer: aborted long readers or unbounded bloat. Then size cleanup so its throughput exceeds the rate dead versions are created, and isolate analytics and change capture so neither pins the writer's horizon.

solid answer

~50 s

Treat retention as a **shared global resource with a single dial**, set by whoever is furthest behind. Three consumers compete for it here: long reports, the change-data-capture consumer's retained stream, and any replicas that report their oldest reader back. **Policy.** Declare a maximum tolerated history depth, say 15 minutes on the writer, and enforce it: statement and transaction timeouts, idle-in-transaction termination, and a cap on retained bytes per slot so an abandoned consumer breaks instead of filling the disk. That is an explicit choice of failure mode: bounded bloat with occasional aborted readers, rather than happy readers and an unbounded relation. **Capacity.** Cleanup throughput must exceed dead-version generation. Measure both; give cleanup enough I/O budget and per-table aggressiveness on the hot relations, and watch for the spiral where bloat slows cleanup and slows it further. **Isolation.** Analytics on a dedicated replica with its own retention; partition by time so expiry is a DROP. **Observability.** Alert on oldest-snapshot age, retained bytes per slot, and the bloat ratio trend.

go deeper

for a junior

At minimum, know that long transactions and stalled consumers hold history and that someone must bound them.

for a middle

Name the retention holders and the mechanical limits: statement and transaction timeouts, idle-in-transaction termination, slot caps.

for a senior

Add the capacity argument (cleanup rate versus version-generation rate), workload isolation onto a replica, and the four signals you would alert on.

for a principal

Lead with the explicit failure-mode choice and a written retention contract: how deep history is funded, who may hold it, what breaks first, and who is paged.

## Frame it as one shared dial Version retention is not a per-query setting. The engine keeps everything newer than the oldest snapshot in use, so the depth of history is determined by whichever consumer is furthest behind. In the described system three consumers can hold it: an hour-long report, a change-data-capture slot whose consumer may stall, and (if replicas exist and report their oldest reader upstream) replica queries. Any of them, unbounded, makes the writer's storage unbounded. The first architectural move is to say out loud that this dial is a platform-level resource and that no individual client gets to set it. ## Choose the failure mode deliberately There are exactly two, and you are always picking one: - **Unbounded retention.** No reader is ever aborted for missing history. Bloat grows with the oldest reader; the eventual failure is disk exhaustion or a table too large to scan, and it hurts everything at once. - **Bounded retention.** History deeper than a chosen window may be reclaimed. Readers that outlive the window die with a targeted error, or a stalled slot is dropped. The failure is loud, attributable and confined. For a platform with an SLO on the transactional workload, bounded is almost always right: it converts a shared, unbounded, silent risk into a per-consumer, bounded, visible one. Write the chosen number down, for example "the writer retains at most 15 minutes of history", and make every long-reader design justify itself against it. ## Enforce mechanically, not by convention Policies that depend on developers keeping transactions short fail. Enforce with: - a maximum statement duration and a maximum transaction duration on the transactional role; - termination of sessions idle inside an open transaction (connection pools with autocommit off are the usual source); - a cap on bytes retained per replication or change-capture slot, so an abandoned consumer is broken rather than allowed to fill the primary; recreating a slot and re-snapshotting is a recoverable incident, a full disk on the writer is not; - an explicit decision on standby feedback: on means replica reports never get cancelled but the primary bloats; off means the primary is protected and long replica queries may be cancelled. Pick per replica according to what that replica is for; - an audit for orphaned two-phase prepared transactions, which pin history indefinitely and survive restarts. ## Size cleanup as a capacity problem Cleanup must reclaim at least as fast as the workload creates dead versions, averaged over the window you can tolerate. So measure both sides: dead versions generated per second (roughly, update plus delete rate, multiplied by the number of indexes for the index work) and the rate cleanup achieves on the hot relations. If the ratio is under one, you will bloat no matter how good the horizon policy is. Give cleanup enough I/O budget and parallelism to win, but not so much that it competes with the transactional workload at peak. The usual shape is a generous budget with per-table aggressiveness raised on the few hot relations, rather than a globally aggressive setting that taxes everything. Be alert to the **degradation spiral**: bloat means more pages to scan, which slows the cleanup pass, which lets bloat grow. Once you are in it, tuning is insufficient and you must reclaim space to get back to a stable regime. ## Isolate the competing workloads - **Analytics on its own replica**, with its own retention budget and feedback disabled toward the primary, so an hour-long report costs storage where it is cheap and cancellations where they are tolerable. - **Change capture treated as a monitored dependency**, not a fire-and-forget subscriber: alert on consumer lag and retained bytes long before the cap, with a documented runbook for dropping and re-seeding. - **Partition by time on the high-churn tables.** Expiry becomes a partition DROP, which produces zero dead versions and returns space instantly; cleanup work per partition stays bounded; and a bloated partition can be reorganised without touching the rest. - **Schema shape.** Keep hot-updated columns in narrow tables, avoid updating indexed columns unnecessarily so in-page update optimisations apply, and debounce counter-style writes. Every version not created is cleanup work not needed. ## Observability and the numbers you defend Four signals, alerted on trend rather than absolute value: age of the oldest open transaction (should be flat and small), retained bytes per slot, bloat ratio per hot relation, and cleanup lag or last-successful-pass age per relation. These map one-to-one onto the three consumers and the capacity side, so any page tells you immediately which lever to pull. ## The principal-level point The deliverable is not a set of parameter values; it is a written contract: how much history the platform funds, who is allowed to hold it, what breaks first when someone exceeds it, and who gets paged. Parameter tuning follows from that contract and will be re-derived every time hardware or workload changes.

  • The change-data-capture consumer stalls overnight and its retained stream is growing fast. Do you drop the slot?
    If the retained volume threatens the writer, yes: breaking one consumer is preferable to filling the disk of the system of record, and re-seeding a change stream is a known, bounded recovery. The decision should be pre-agreed and encoded as a retention cap with alerting well before the cap, so the choice is made in daylight rather than during an incident. What you must not do is let an unattended subscriber hold the writer's history indefinitely.
  • Should standby feedback to the primary be on or off?
    It depends on what the replica is for. On means long replica queries are not cancelled, at the cost of pinning the primary's cleanup horizon and bloating it. Off protects the primary and accepts that replica queries can be cancelled when the replica must apply conflicting changes. For a reporting replica whose reports can be retried or chunked, off is usually right; decide per replica and document it.

saying these in an interview costs you the question

  • Answering with parameter values instead of a retention contract and enforcement
  • Assuming a replica removes the pin when feedback to the primary is enabled
  • Treating a stalled change-capture consumer as harmless to the writer
  • Ignoring the capacity side: cleanup throughput versus dead-version generation rate
  • Relying on developers to keep transactions short rather than enforcing limits

context