Lifecycle and tiering: cheap because bytes are immutable

Storage classes trade per-GB cost for retrieval latency, a retrieval fee, and a minimum-duration lock-in, and lifecycle rules slide objects down the ladder automatically — cheap to build precisely because objects are immutable and the index is the source of truth for location and class.

Previously

Reaping orphaned parts was one declarative lifecycle action — and the same lifecycle machinery drives the bigger cost lever: sliding objects to cheaper storage as they cool.

Scene 10

Lifecycle and tiering: cheap because bytes are immutable

  1. Watch
  2. Try it
  3. Predict
  4. Capture
storage-class ladderhot → archive · lower cost trades for higher retrieval latencyHOTCOLDStandardretrievalms$/GB$$$min-durationno minimumStandard-IAretrievalms$/GB$$min-duration30-day minGlacier Flexibleretrievalminutes-hours$/GB$min-duration90-day minGlacier Deep Archiveretrievalhours$/GBcentsmin-duration180-day minOBJECTphotos/2021/im…key unchangedper-GB cost$$$—highest (hot)retrieval latencyms—instant (ms)LIFECYCLE RULEdeclarative · enforced by a background workertransition after 30d → next colder classexpire after 365d → delete markerreap incomplete MPU after 7dincomplete-MPU billing meterorphaned parts still billed (enable cleanup, s3-08)
Storage class = a tier with its own per-GB cost, retrieval latency, and minimum-duration lock-in.
What to watch for

The object starts on Standard — the hot rung: ms reads, highest per-GB cost, no minimum-duration commitment. A lifecycle rule says "transition after 30 days." Watch the day counter: when it fires, a background worker rewrites the bytes one rung colder and the index pointer follows. The key never changes; no client sees the move — but the retrieval-latency readout climbs.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

LifecycleWorker.scan
background fleet sweeps the prefix and fires due rules
# runs continuously over each bucket/prefix
for obj in index.scan(prefix):
age = now() - obj.createdAt
if age >= rule.transitionAfterDays:
transition(obj, rule.toClass) # slide one rung colder
if age >= rule.expireAfterDays:
expire(obj) # delete current version
# orphaned multipart parts never showed up in LIST
for mpu in index.incompleteUploads(prefix):
if mpu.age >= rule.reapIncompleteMpuAfterDays:
abortUpload(mpu) # parts leave billing meter
Worker.transition
the move is a byte rewrite plus an index-pointer flip
def transition(obj, toClass):
# immutable bytes => no in-place edit to coordinate
newLoc = dataPlane.rewrite(
obj.fragments, toClass.medium,
)
# index is the source of truth for location + class
index.update(obj.key, {
location: newLoc,
storageClass: toClass,
}) # key unchanged; no client sees the move
dataPlane.free(obj.oldLocation)
Billing.onDelete
the cold rate is bought with a minimum-duration lock-in
def onDelete(obj):
stored = now() - obj.classEnteredAt
minDays = obj.storageClass.minDurationDays
# cold classes bill the floor even if you delete early
billedDays = max(stored, minDays)
charge(obj, billedDays)

Where this sits in Build an S3-style distributed object store

Scene 10 of 12, in the Make it real act — Strong consistency, multipart upload, lifecycle & tiering.. Storage classes trade retrieval latency for cost; a lifecycle rule slides an object down the ladder as a pointer move.

Up next. We've now built every piece — keyspace, immutability, erasure coding, placement, the two planes, repair, consistency, multipart, lifecycle — so the last move is to assemble them for a real workload and defend the durability number.

All 12 scenes in Build an S3-style distributed object store · Every curriculum

Built with Arqly
Every scene in Build an S3-style distributed object store builds on the one before it.All 12 Build an S3-style distributed object store scenes