---
title: 'Storage Classes'
source: 'https://academia.sh/en/courses/cloud-storage/storage-classes'
course: 'Storage and Data in the Cloud'
language: en
updated: '2026-09-13T05:07:37+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows in the class table that storage and access fees run in opposite directions'
    - 'Scans the break-even access frequency and counts how object size shifts it'
    - 'Distributes classes over access history: compares single-class strategies against per-object selection'
    - "Counts the wrong class's cost both ways and records the minimum retention debt as an unrecoverable item"
---

# Storage Classes

Measuring tiers by access frequency: how the storage fee and the access fee run in opposite directions, scanning the break-even access frequency by object size, distributing objects into classes over access history, and counting the wrong class both ways — the annual cost of keeping infrequent data in the frequent class, and frequent data in archive.

The previous lesson billed the four families in the fictional bucket at a single storage rate. Yet
the families do not resemble each other: meter readings are small and numerous, invoice documents
are large and rarely read, field photos are written once and almost never opened again. Object
storage carries this difference over to the price side with the **storage class**: objects in the
same bucket can be kept in different classes, and the class is chosen per object.

Class is not a quality dial. Durability is identical across all four classes; the only thing that
changes is how the fee splits between storage and access. So the sentence "the infrequent class is
cheaper" carries no decision; the number that carries the decision is the **break-even access
frequency**.

- **ST7** — The four classes and their fees are fictional; classes are called only by access
  frequency, fees are denominated in a fictional **unit**.
- **ST8** — Access history is a mixture model: the hot share and the hot and cold annual averages
  are written into the model, the generator lives in the model file, the seed is 40127.
- **ST9** — Class is chosen per object; the annual read count stays fixed across the year.

```js
// class/class-model.mjs — storage classes and access history are a MODEL. Fees are a fictional
// unit; classes are called by access frequency, no commercial name is used.
export const CLASS = [                 // storage: unit/GiB/month, request and transition: unit/request, retrieval: unit/GiB
  { name: "frequent", storage: 0.0230, request: 0.00040 / 1000, retrieval: 0.000, minDays: 0, transition: 0.0050 / 1000 },
  { name: "infrequent", storage: 0.0125, request: 0.00100 / 1000, retrieval: 0.010, minDays: 30, transition: 0.0100 / 1000 },
  { name: "rare", storage: 0.0040, request: 0.00250 / 1000, retrieval: 0.020, minDays: 90, transition: 0.0250 / 1000 },
  { name: "archive", storage: 0.0010, request: 0.00500 / 1000, retrieval: 0.030, minDays: 180, transition: 0.0500 / 1000 },
];
export const EGRESS_INTERNET = 0.085;  // unit/GiB, fictional

// Access history is a mixture model: every family has a hot share, the rest is cold.
export const FAMILIES = [   // count, average size, hot share, hot and cold annual read average
  { name: "meter-readings", count: 1_240_000, avg: 2400, hotShare: 0.06, hotAvg: 60, coldAvg: 0.4 },
  { name: "invoice-documents", count: 86_000, avg: 380 * 1024, hotShare: 0.15, hotAvg: 26, coldAvg: 0.8 },
  { name: "field-photos", count: 42_000, avg: Math.round(3.6 * 1024 * 1024), hotShare: 0.03, hotAvg: 40, coldAvg: 0.15 },
  { name: "work-order-records", count: 310_000, avg: 11 * 1024, hotShare: 0.20, hotAvg: 34, coldAvg: 1.2 },
];
export const gib = (bytes) => bytes / 1024 ** 3;
// Annual fee: storage + read requests + retrieved bytes. Changing class is a separate fee.
export const annualFee = (c, bytes, readsPerYear) =>
  12 * c.storage * gib(bytes) + readsPerYear * (c.request + gib(bytes) * c.retrieval);

export const SEED = 40127;             // we write the generator ourselves, the seed is visible: fictional
export function* accessPattern(family) {   // annual read count for each object (integer)
  let x = (SEED ^ family.count) >>> 0;
  const u = () => (x = (Math.imul(x, 1664525) + 1013904223) >>> 0) / 4294967296;
  for (let i = 0; i < family.count; i++) {
    const avg = u() < family.hotShare ? family.hotAvg : family.coldAvg;
    yield Math.floor(-avg * Math.log(1 - u()));
  }
}
```

## Break-Even Access Frequency

```js
// class/break-even.mjs — storage and access fees run in opposite directions: the break-even access frequency is scanned
import { CLASS, FAMILIES, gib, annualFee } from "./class-model.mjs";

const g = (x, n, d = 2) => x.toFixed(d).padStart(n);
console.log("class                | storage |  request (per 1,000) |  retrieval unit/GiB | min retention");
console.log("---------------------|---------|----------------------|---------------------|--------------");
for (const c of CLASS)
  console.log(`${c.name.padEnd(20)} | ${g(c.storage, 7, 4)} | ${g(c.request * 1000, 20, 5)} | ` +
    `${g(c.retrieval, 19, 3)} | ${String(c.minDays + " days").padStart(13)}`);
console.log("As storage drops, request and retrieval rise: there is no single dial to turn.");

// Break-even scan: with a step of 0.1 reads a year, where the cheap-storage class turns expensive.
const breakEven = (cheap, costly, bytes) => {
  for (let k = 0; k <= 2000; k++) {
    const r = k / 10;
    if (annualFee(cheap, bytes, r) > annualFee(costly, bytes, r)) return r;
  }
  return null;
};
console.log("\nbreak-even access frequency (reads a year); above it the cheap-storage class turns costly:");
console.log("family             | avg size   | infrequent>frequent  | rare>infrequent  | archive>rare ");
console.log("-------------------|------------|----------------------|------------------|-------------");
for (const f of FAMILIES) {
  const c = [breakEven(CLASS[1], CLASS[0], f.avg), breakEven(CLASS[2], CLASS[1], f.avg),
    breakEven(CLASS[3], CLASS[2], f.avg)];
  const size = f.avg < 1024 ** 2 ? (f.avg / 1024).toFixed(1) + " KiB" : (f.avg / 1024 ** 2).toFixed(1) + " MiB";
  console.log(`${f.name.padEnd(18)} | ${size.padStart(10)} | ` +
    c.map((x, i) => x.toFixed(1).padStart([20, 16, 13][i])).join(" | "));
}
console.log("Break-even drops for small objects: the request fee outweighs the byte fee, and cheap");
console.log("storage only pays off within a very narrow frequency range.");

// Same family, same bytes, two different frequencies: the point where the ranking reverses.
const f = FAMILIES[1], y = (r) => CLASS.map((c) => annualFee(c, f.avg, r) * f.count);
const [t0, t12] = [y(0), y(12)];
console.log(`\n${f.name} (${gib(f.count * f.avg).toFixed(1)} GiB) annual total, fictional unit:`);
console.log(`  zero reads   : frequent ${g(t0[0], 6)} | archive ${g(t0[3], 6)} -> archive ${(t0[0] / t0[3]).toFixed(0)}x cheaper`);
console.log(`  12 reads/year: frequent ${g(t12[0], 6)} | archive ${g(t12[3], 6)} -> ranking reversed`);
```

```
class                | storage |  request (per 1,000) |  retrieval unit/GiB | min retention
---------------------|---------|----------------------|---------------------|--------------
frequent             |  0.0230 |              0.00040 |               0.000 |        0 days
infrequent           |  0.0125 |              0.00100 |               0.010 |       30 days
rare                 |  0.0040 |              0.00250 |               0.020 |       90 days
archive              |  0.0010 |              0.00500 |               0.030 |      180 days
As storage drops, request and retrieval rise: there is no single dial to turn.

break-even access frequency (reads a year); above it the cheap-storage class turns costly:
family             | avg size   | infrequent>frequent  | rare>infrequent  | archive>rare 
-------------------|------------|----------------------|------------------|-------------
meter-readings     |    2.3 KiB |                  0.5 |              0.2 |           0.1
invoice-documents  |  380.0 KiB |                 10.9 |              7.3 |           2.2
field-photos       |    3.6 MiB |                 12.4 |              9.8 |           3.4
work-order-records |   11.0 KiB |                  1.9 |              0.7 |           0.2
Break-even drops for small objects: the request fee outweighs the byte fee, and cheap
storage only pays off within a very narrow frequency range.

invoice-documents (31.2 GiB) annual total, fictional unit:
  zero reads   : frequent   8.60 | archive   0.37 -> archive 23x cheaper
  12 reads/year: frequent   9.01 | archive  16.75 -> ranking reversed
```

The break-even table shows that class choice depends on **object size**. If a 380 KiB invoice
document stays under 10.9 reads a year, the infrequent class pays off; for a 2.3 KiB meter reading
the same threshold is 0.5 — putting a record that is read once a year into the cheap-storage class is
a loss. The reason is the flat request fee: for a small object, the cost of access is measured not in
bytes but in **request count**. The last two lines show the same thing on a single family.

## Distribution over Access History

- **ST10** — Changing class is a transition request, billed separately, and the minimum retention
  period starts at the transition.
- **ST11** — The retrieval fee is separate from the egress fee; both are paid together for the same
  bytes.
- **ST12** — In the model, an object read more than 24 times a year counts as "hot."

```js
// class/distribution.mjs — classes are distributed over access history; the wrong class is counted both ways
import { CLASS, FAMILIES, EGRESS_INTERNET, gib, annualFee, SEED, accessPattern } from "./class-model.mjs";

const g = (x, n, d = 2) => x.toFixed(d).padStart(n);
const cheapest = (bytes, r) => {
  const t = CLASS.map((c) => annualFee(c, bytes, r));
  return t.indexOf(Math.min(...t));
};
// Single pass: every family's access history is scanned, summaries and strategy totals come out together.
const SUMMARY = [], STRATEGY = { "all frequent": 0, "all infrequent": 0, "all archive": 0, "by family": 0, "per object": 0 };
let neverRead = 0, neverReadBytes = 0, neverReadExcess = 0;
let hotInArchive = 0, hotInArchiveReads = 0, hotInArchiveBytes = 0, hotInArchiveExcess = 0, retrievedBytes = 0;

for (const f of FAMILIES) {
  let totalReads = 0, zero = 0, distribution = [0, 0, 0, 0], perObject = 0;
  const familyTotal = CLASS.map(() => 0), threshold = 24;   // an object read more than 24 times a year counts as hot
  for (const r of accessPattern(f)) {
    totalReads += r;
    if (r === 0) zero++;
    const i = cheapest(f.avg, r);
    distribution[i]++;
    perObject += annualFee(CLASS[i], f.avg, r);
    for (let k = 0; k < CLASS.length; k++) familyTotal[k] += annualFee(CLASS[k], f.avg, r);
    if (r === 0) {                        // a never-read object kept in the frequent class
      neverRead++; neverReadBytes += f.avg;
      neverReadExcess += annualFee(CLASS[0], f.avg, 0) - annualFee(CLASS[3], f.avg, 0);
    } else if (r > threshold) {           // a hot object dropped into archive
      hotInArchive++; hotInArchiveReads += r; hotInArchiveBytes += f.avg; retrievedBytes += r * f.avg;
      hotInArchiveExcess += annualFee(CLASS[3], f.avg, r) - annualFee(CLASS[0], f.avg, r);
    }
  }
  const best = familyTotal.indexOf(Math.min(...familyTotal));
  STRATEGY["all frequent"] += familyTotal[0]; STRATEGY["all infrequent"] += familyTotal[1];
  STRATEGY["all archive"] += familyTotal[3]; STRATEGY["by family"] += familyTotal[best];
  STRATEGY["per object"] += perObject;
  SUMMARY.push({ f, totalReads, zero, distribution });
}

console.log(`access history is a model; the generator is in this file, seed ${SEED}. Annual read counts:`);
console.log("\nfamily             | never read | avg reads/yr | cheapest class per object");
console.log("                   |            |              |  frequent | infrequent |  rare | archive");
console.log("-------------------|------------|--------------|-----------|------------|-------|--------");
for (const o of SUMMARY)
  console.log(`${o.f.name.padEnd(18)} | ${g(100 * o.zero / o.f.count, 9, 1)}% | ${g(o.totalReads / o.f.count, 12, 2)} | ` +
    o.distribution.map((x, i) => String(x).padStart([9, 10, 5, 7][i])).join(" | "));

console.log("\nstrategy        | annual total | excess over best  | share of excess");
console.log("----------------|--------------|-------------------|----------------");
const best = STRATEGY["per object"];
for (const [name, t] of Object.entries(STRATEGY))
  console.log(`${name.padEnd(15)} | ${g(t, 12)} | ${g(t - best, 17)} | ${g(100 * (t - best) / best, 14, 1)}%`);

console.log("\nthe wrong class, both ways:");
console.log(`  cold data in the frequent class: ${neverRead.toLocaleString("en-US")} objects, ` +
  `${g(gib(neverReadBytes), 0, 1)} GiB never read; keeping them in the frequent class instead of archive costs ` +
  `${g(neverReadExcess, 0, 2)} units a year extra.`);
console.log(`  hot data in the archive class: ${hotInArchive.toLocaleString("en-US")} objects, ` +
  `${g(gib(hotInArchiveBytes), 0, 1)} GiB, ${hotInArchiveReads.toLocaleString("en-US")} reads a year; ` +
  `keeping them in archive costs ${g(hotInArchiveExcess, 0, 2)} units a year extra.`);
console.log(`  if the ${g(gib(retrievedBytes), 0, 1)} GiB retrieved by these reads also leaves to the internet, egress adds ` +
  `${g(gib(retrievedBytes) * EGRESS_INTERNET, 0, 2)} units on top: the retrieval fee does not replace egress, it stacks on it.`);

// Changing class is reversible, not free: it carries the minimum retention period.
const f = FAMILIES[1], N = 12_000, DAY = 8, infrequent = CLASS[1];
const transitionFee = 2 * N * infrequent.transition;
const minRetentionDebt = N * infrequent.storage * gib(f.avg) * (infrequent.minDays / 30);
const retrievalFee = N * gib(f.avg) * infrequent.retrieval;
const savings = N * (CLASS[0].storage - infrequent.storage) * gib(f.avg) * (DAY / 30);
console.log(`\n${N.toLocaleString("en-US")} invoice documents moved to infrequent, found hot on day ${DAY}, moved back:`);
console.log(`  round-trip transition request ${g(transitionFee, 0, 3)} | min ${infrequent.minDays}-day retention debt ${g(minRetentionDebt, 0, 3)} | ` +
  `retrieval ${g(retrievalFee, 0, 3)}`);
console.log(`  total ${g(transitionFee + minRetentionDebt + retrievalFee, 0, 3)} units; saved over ${DAY} days ${g(savings, 0, 3)} units; ` +
  `net ${g(transitionFee + minRetentionDebt + retrievalFee - savings, 0, 3)} units loss.`);
console.log(`  the decision was reversed, the fee was not: minimum retention debt stands even if the object is deleted.`);
```

```
access history is a model; the generator is in this file, seed 40127. Annual read counts:

family             | never read | avg reads/yr | cheapest class per object
                   |            |              |  frequent | infrequent |  rare | archive
-------------------|------------|--------------|-----------|------------|-------|--------
meter-readings     |      86.4% |         3.66 |    168911 |          0 |     0 | 1071089
invoice-documents  |      61.4% |         4.16 |      8311 |       1095 |  3774 |   72820
field-photos       |      97.0% |         1.22 |       920 |         64 |   142 |   40874
work-order-records |      45.7% |         7.34 |    105257 |      63012 |     0 |  141731

strategy        | annual total | excess over best  | share of excess
----------------|--------------|-------------------|----------------
all frequent    |        53.91 |             45.78 |          563.0%
all infrequent  |        38.39 |             30.26 |          372.2%
all archive     |        48.65 |             40.52 |          498.4%
by family       |        16.80 |              8.67 |          106.6%
per object      |         8.13 |              0.00 |            0.0%

the wrong class, both ways:
  cold data in the frequent class: 1,306,318 objects, 166.2 GiB never read; keeping them in the frequent class instead of archive costs 43.87 units a year extra.
  hot data in the archive class: 84,485 objects, 4.6 GiB, 6,192,838 reads a year; keeping them in archive costs 35.56 units a year extra.
  if the 276.3 GiB retrieved by these reads also leaves to the internet, egress adds 23.48 units on top: the retrieval fee does not replace egress, it stacks on it.

12,000 invoice documents moved to infrequent, found hot on day 8, moved back:
  round-trip transition request 0.240 | min 30-day retention debt 0.054 | retrieval 0.043
  total 0.338 units; saved over 8 days 0.012 units; net 0.326 units loss.
  the decision was reversed, the fee was not: minimum retention debt stands even if the object is deleted.
```

## The Wrong Class's Two Directions

The distribution column confirms the break-even table: for meter readings the two middle classes
take zero objects, and the 1,240,000 records split between frequent and archive. As objects get
larger the middle classes' range opens up, and for invoice documents all four are used.

The strategy table shows that all three single-class approaches lose. Keeping everything in the
frequent class is 6.6x the best distribution. Dropping everything into archive is, counter to
intuition, more expensive than keeping everything in the infrequent class — 48.65 against 38.39 —
because the retrieval fee of the hot objects swallows what was saved. A single class at the family
level brings the total down to 16.80, but that is still 106% in excess; half the gain lies in the
**hot–cold split within each family**.

The two directions are counted separately. The 1,306,318 objects that are never read — 166.2 GiB —
cost 43.87 units a year extra when kept in the frequent class; this direction is silent, it throws no
error. The reverse direction holds less data but costs more money: the 84,485 objects read more than
24 times a year are only 4.6 GiB, but keeping them in archive costs 35.56 units a year extra. If the
retrieved 276.3 GiB also leaves to the internet, a 23.48-unit egress fee stacks **on top** of that.

## The Decision Reversed, the Fee Not

Unlike key layout, storage class looks reversible: one request moves an object to another class. The
last block counts the cost. 12,000 invoice documents are moved to the infrequent class and moved back
on day eight: 0.240 transition request, 0.054 minimum retention debt, 0.043 retrieval, total 0.338
units. What was saved over eight days is 0.012 units; the net loss is twenty-seven times the saving.

**The unrecoverable item is the minimum retention debt.** The moment an object enters a class, that
class's period is committed; the debt stands whether the object is moved or deleted. The delegated
side is constant: durability, replication, and placement belong to the provider in all four classes.

## Summary

- Class is not a quality dial but a trade-off: as storage drops, request and retrieval rise,
  durability does not change.
- Break-even frequency depends on object size: 10.9 reads a year for a 380 KiB document, 0.5 reads
  for a 2.3 KiB record. For small objects the middle classes never pay off.
- Per-object selection costs 8.13 units; keeping everything in the frequent class costs 53.91,
  dropping everything into archive costs 48.65 — archive is not automatically cheap.
- The wrong class costs in both directions: 166.2 GiB that is never read costs 43.87 units a year
  extra in the frequent class, and 4.6 GiB that is read often costs 35.56 units a year extra in
  archive. The retrieval fee stacks on top of egress.
- The class decision is reversible, its fee is not: a transition reversed in eight days leaves a loss
  twenty-seven times the saving; minimum retention debt stands even if the object is deleted.

## Next Step

This lesson's class distribution was done by hand: every object's annual read count was known, and
the cheapest class was chosen. In a real bucket this information is not known in advance; a script
that moves every object one by one carries away the gain in the fee of the requests it makes. The
next lesson hands the decision to the store: a lifecycle rule changes an object's class and deletes
it by age. Three things will be measured — the bytes that accumulate without a rule, the number of
objects transitioned and deleted by the rule, and an early transition's retrieval cost. Versioning is
also introduced in the same lesson: once turned on, it is shown with a number that deletion does not
actually delete.
