There is no “best” storage. There is only fit — and the bill follows the fit.
Every storage service on every cloud exists because some workload has an access shape: how big the unit of data is, how often you touch it, whether many machines touch it at once, whether readers must agree, and how fast you need it back. Match the shape and things are cheap and fast. Mismatch it and you get one of two failure modes, and only one of them is visible on a dashboard:
Broken latency
You put a database's write-ahead log on object storage, or you serve interactive reads from an archive tier. The application is slow or simply doesn't work. You find out in minutes.
10× cost
You keep 400 TB of never-read logs on the hot tier, or you archive backups you delete after 30 days and pay the 90-day minimum on every one. Nothing breaks. You find out at month end, or never.
Two maps you will use for the rest of your career. The top row is raw storage — three access shapes. The bottom block is the database zoo — engines that add concurrency control and query on top of that raw storage so your application doesn't have to. Hover or tap any box for what it is.
Each tab is self-contained and follows the same five beats: why it exists → core concepts (neutral, then three clouds) → reality check with real numbers → apply it to your GenAI stack → bridge.
| Source | You said | Actual section title | Verdict |
|---|---|---|---|
| the cloud application patterns catalog a cloud patterns catalog | §7 | §7 — Cloud-Native Storage | Correct. Root pattern is Cloud Database; thirteen patterns. It is the neutral spine for S1. |
| AWS architecture guidance AWS guidance | §5 | §5 — AWS Service Overview | Correct number, but §5.3 Storage / §5.4 Databases is a service catalogue, not a depth section. Supplemented — see note below. |
| Mastering Azure vendor guidance, vendor guidance, vendor guidance | §6 + §11 | §6 — Storage; §11 — Databases and Big Data Solutions | Correct. §6 is the strongest storage section of the four (accounts, tiers, LRS/ZRS/GRS/GZRS, disks, Elastic SAN). |
| Ultimate Google Professional Cloud Architect GCP guidance | §3 + §9–10 | §3 — GCP Storage Infrastructure (Block, File, and Object); §9 — Databases in GCP Overview; §10 — Relational, NoSQL, and Caching Databases | Correct. One addition: §13 — Exploring Data Warehousing and Data Lifecycle on GCP is the natural source for the S4 analytics section, so I pulled from it too. |
Cert guides age fastest on exactly the three things this stage is about: storage class names, database product names, and per-GB / per-request prices. Here is the stage-level ledger; each tab repeats the relevant rows with sources.
| Area | What a 2024-era source says | Current as of July 2026 |
|---|---|---|
| GCP block | Persistent Disk types (pd-standard, pd-balanced, pd-ssd, pd-extreme) | Hyperdisk is the recommended family and Persistent Disk is not available on the newest machine series at all. Types: Balanced, Balanced High Availability, Extreme, Throughput, and Hyperdisk ML — a read-only volume attachable to many instances, built for model weights and training data. |
| GCP object | Four classes: Standard / Nearline / Coldline / Archive | Still four for the temperature ladder, plus a zonal Rapid storage class (Rapid Bucket) aimed at AI/ML, plus Rapid Cache (the renamed Anywhere Cache) and Autoclass. Multi-region Nearline went up and multi-region Archive went down in 2026 — old lifecycle policies may no longer be optimal. |
| AWS relational | RDS, Aurora, Aurora Serverless | Aurora Serverless v2 now scales to 0 ACUs, and Aurora DSQL — a serverless, active-active, multi-region-strongly-consistent PostgreSQL-compatible distributed SQL database — went GA on 27 May 2025. It is AWS's answer to Spanner and it is not in any of the materials. |
| AWS NoSQL | Provisioned is much cheaper than on-demand | AWS cut on-demand throughput prices by 50% (Nov 2024) and added Database Savings Plans (Dec 2025). The old “always start provisioned” advice is now wrong for most spiky workloads. DynamoDB global tables also gained multi-region strong consistency (GA June 2025). |
| Azure cache | Azure Cache for Redis | Azure Managed Redis is the successor. Azure Cache for Redis Enterprise retires 30/31 March 2027; Basic, Standard and Premium retire 30 Sept 2028. Any exam answer or design that names “Azure Cache for Redis” for new build is already stale. |
| Azure analytics | Azure Synapse Analytics is the warehouse | Synapse is supported but is maintained, not evolved; Microsoft Fabric is where net-new investment goes. Specific components are already retired (Synapse Data Explorer → Fabric Eventhouse; Synapse Link for Cosmos DB → Cosmos DB mirroring in Fabric). |
| GCP databases | Cloud Spanner, Firestore, Memorystore for Redis/Memcached | Spanner now sells in editions (Standard / Enterprise / Enterprise Plus) and is multi-model (graph, full-text, vector). Firestore has Standard and Enterprise editions with MongoDB compatibility. Memorystore for Valkey joined Redis and Memcached. |
You can decide
- Block, file or object for a given piece of data — from the access shape, not from habit.
- SQL vs document vs key-value vs wide-column vs cache for a given workload.
- Regional vs multi-region vs global for a database, deliberately, knowing the write-latency price.
- Which object storage class, including whether the retrieval and minimum-duration rules kill the saving.
You can translate
- S3 ⇄ Blob ⇄ Cloud Storage · EBS ⇄ Managed Disks ⇄ Hyperdisk · EFS ⇄ Files ⇄ Filestore.
- RDS/Aurora ⇄ Azure SQL ⇄ Cloud SQL/AlloyDB · DynamoDB ⇄ Cosmos DB ⇄ Firestore/Bigtable.
- Durability/availability/consistency claims into replica topology.
- Your own stack: weights, datasets, checkpoints and pipeline state onto the right service on each cloud.
Session 1 is entirely vendor-neutral on purpose. Everything in S2–S4 is a dialect of it; if S1 lands, the other three tabs are mostly vocabulary.
Storage is dumb on purpose. Databases are the thing that makes it usable.
the patterns catalog's Cloud-Native Storage section opens with the problem you inherited from C3: a stateless application still has state, so where does it go? His answer is blunt — raw storage looks tempting, and it is ultimately where every byte lands, but raw storage fails applications on two counts.
Concurrency
Storage handles one reader or one writer beautifully. Two threads writing the same region at once corrupt it; a reader mid-write sees a torn value. And you always have concurrency, because C3's replicas of your service all share the same data.
Querying
Storage can hand you a thing if you know exactly where it is. Ask it “which rows match this predicate” and the only implementation is: read everything, filter in your app. That is fine at 10 MB and ruinous at 10 TB.
So this session teaches two layers at once — the raw storage taxonomy (block / file / object), and the database zoo that sits on top of it — plus the three axes (durability, availability, consistency) you trade along in both. Vendor-neutral throughout. No console, no service names except as illustrations, until Session 2.
Three ways to be given somewhere to put things.
An empty warehouse floor with numbered bays. Nobody organises it for you. You bring your own shelving system (a filesystem), you decide what goes in bay 4,192, and you're the only tenant with a key. Fastest possible access because there's no middleman.
A shared office filing cabinet. Folders, labels, a rule that two people can't scribble on the same page at once. Everyone in the building can walk up and use it. Slower than your own drawer, but everyone speaks the same language.
A coat check. You hand over a whole coat and get a ticket. To change a button you take the whole coat back, sew, and hand in a whole new coat. Unlimited racks, dirt cheap, and there's a cheaper long-term room out back with a delay to fetch from it.
The difference is what the API's unit of work is, and that single fact cascades into everything else.
- IOPS
- Input/output operations per second — how many separate reads or writes a device sustains. The metric that matters when operations are small and scattered (a database index lookup).
- Throughput
- Bytes per second, usually MB/s or GB/s. The metric that matters when operations are large and sequential (streaming a 40 GB checkpoint).
- Latency
- Time for one operation to complete. Block ≈ tens–hundreds of microseconds; file ≈ sub-millisecond to low milliseconds; object ≈ tens of milliseconds for first byte.
- LBA
- Logical block address — the “bay number” in block storage. The unit the device API addresses.
- POSIX
- The Unix file API contract: paths, permissions, seek, partial writes, locks, rename-is-atomic. Software that expects a “real filesystem” is expecting POSIX.
- Namespace
- How names are organised. File storage is hierarchical (a tree). Object storage is flat — one giant dictionary of keys.
- Prefix
- The leading part of an object key (
logs/2026/07/). Used for pseudo-folders, lifecycle scoping and request-rate partitioning.
| Dimension | Block | File | Object |
|---|---|---|---|
| Unit of write | A block (512 B – 4 KiB) | A byte range in a file | The whole object |
| Concurrent writers | One machine (multi-attach exists but needs a cluster-aware filesystem) | Many, arbitrated by the service | Many, but last-write-wins on the same key |
| Typical latency | ~0.1–1 ms | ~0.5–10 ms | ~10–100 ms first byte |
| Order-of-magnitude cost / GB-month | $0.08 – $0.13 | $0.16 – $0.30 hot tier | $0.001 – $0.023 |
| Capacity ceiling | Per-volume cap (tens of TB) | Effectively elastic | Effectively unlimited |
| Failure domain by default | One zone | One or many zones (choose) | Multi-zone, often multi-region |
| Breaks when… | You need two machines to write, or you exceed the volume's provisioned IOPS and every query stalls behind the queue | You do millions of small-file metadata ops, or you assume it's as cheap as object storage | You need to modify part of a large file, need <5 ms reads, or your access pattern is millions of tiny objects (per-request fees and per-object metadata overhead dominate) |
Advanced nuance · the three “gotcha” facts most people learn the hard way
1. Object storage is not a filesystem, and mounting it doesn't make it one. FUSE-style mounts (s3fs, Cloud Storage FUSE, BlobFuse) present a bucket as a path. They are genuinely useful — Google reports large training-throughput gains from theirs — but under the hood a partial write is still a full object rewrite and a directory listing is still a prefix scan. Never put a database, a git repo, or an SQLite file on one.
2. Provisioned block performance is a queue, not a speed limit you politely approach. If a volume is provisioned for 3,000 IOPS and your workload asks for 6,000, you don't get “half speed”; you get a queue, and queue depth turns into multi-second p99 latency. Storage saturation almost always presents as an application timeout, not a storage alarm.
3. Small objects are the silent killer. Archive tiers add fixed metadata overhead per object (on S3's Flexible Retrieval and Deep Archive, about 40 KB), and every class bills per request. A hundred million 8 KB objects can cost more in overhead and requests than the bytes themselves. Aggregate small files into shards or tar/WebDataset-style archives before tiering them.
-
Does a single machine need a real filesystem it alone owns — an OS boot disk, a database's data files, a write-ahead log?yes →Block. Provision for IOPS if the I/O is small and random; provision for throughput (MB/s) if it is large and sequential.no ↓
-
Do several machines need to read and write the same paths at the same time, with POSIX semantics and locking?yes →File. The usual honest reason is “the software I did not write insists on a path”. That is a legitimate reason.no ↓
-
Do many readers need the same immutable artifact, with no partial updates — model weights, datasets, images, backups, logs?yes →Object. And now pick the class by access frequency — that is the second tree, in Session 2.no ↓
-
Is the data really records you will query by attribute, update in place, and need transactionally consistent?yes →You are not choosing raw storage at all. Use a database — go to the second decision tree below.no ↓
Three different questions that people constantly mash into the word “reliable”. Your bank:
- Durability — will the bank still know I have £5,000 next year? (Did the bytes survive?)
- Availability — is the ATM working right now? (Can I reach the bytes?)
- Consistency — if I withdraw at one ATM and my partner checks the balance at another one second later, do they see the new number? (Do all readers agree?)
They are independent. A system can be perfectly durable and completely unavailable (your data is safe in a region that is on fire and unreachable). It can be highly available and inconsistent (every replica answers instantly, some with stale values).
- Replication
- Keeping more than one copy of the same data. Synchronous = the write isn't acknowledged until the copies land. Asynchronous = acknowledged first, copied after.
- RPO
- Recovery point objective — how much recent data you accept losing in a disaster, measured in time. Synchronous replication gives RPO = 0. Asynchronous gives RPO = replication lag.
- RTO
- Recovery time objective — how long you accept being down. A different dial from RPO, and usually the more expensive one.
- Durability target
- “Eleven nines” = 99.999999999% annual. Concretely: store 10 million objects and you'd expect to lose one about every 10,000 years. It is a design target from the replication maths, not an SLA with credits.
- Availability SLA
- A contractual monthly uptime percentage with service credits if missed. Typically 99.9%–99.99% for storage, and it is always a lower number than the durability figure.
| Availability | Downtime / month | Downtime / year | What it typically costs you |
|---|---|---|---|
| 99% | 7 h 18 m | 3.65 days | A single instance you patch by hand |
| 99.9% (“three nines”) | 43 m 50 s | 8.77 h | Managed service, single zone |
| 99.95% | 21 m 55 s | 4.38 h | Zone-redundant / Multi-AZ, automatic failover |
| 99.99% (“four nines”) | 4 m 23 s | 52.6 m | Multi-zone with no single write leader, or hot standby |
| 99.999% (“five nines”) | 26 s | 5.26 m | Multi-region active-active. Expensive, and mostly bounded by your code, not the platform's |
The thing nobody tells you · durability ≠ “I can't lose my data”
Eleven nines of durability protects against hardware loss. It does not protect against the four things that actually destroy production data:
- You deleted it. A
rm -rfor a bad lifecycle rule is replicated to all eleven copies, faithfully and instantly. Mitigations: versioning, soft delete, object lock / immutability, and a retention policy you tested. - Your application corrupted it. Same story — perfectly durable garbage. Mitigation: point-in-time restore, which is a database feature, not a storage one.
- Credentials were stolen. Mitigation is C2's, not this stage's: least-privilege IAM, separate delete permissions, MFA-delete, and backups in an account the primary credentials cannot reach.
- You never checked the restore worked. A backup you have not restored is a hypothesis.
Rule of thumb: durability is the platform's job; recoverability is yours.
ACID — the ledger
A bookkeeper with a ruler. Either the whole entry goes in or none of it does; the materials always balance; two clerks can't half-write the same line; once it's inked, it survives a fire. Slower, and worth it when the numbers are money, inventory or medication doses.
BASE — the noticeboard
Anyone can pin a note anywhere, instantly. Different corridors briefly show different notices, and within a few seconds the building agrees. Wildly scalable, and completely fine for a social feed, a view counter, or a product catalogue.
CAP is the referee: when the network splits the building in half, you may keep answering (possibly with stale notes) or stay correct (by refusing to answer). Not both. PACELC is the honest follow-up question: fine — but what do you do the other 99.99% of the time, when the network is healthy? There you trade latency against consistency, and that trade is the one you live with every day.
- ACID
- Atomicity (all or nothing), Consistency (constraints never violated), Isolation (concurrent transactions don't see each other's partial work), Durability (committed means committed).
- BASE
- Basically Available, Soft state, Eventually consistent. Not a weaker ACID — a different contract: prioritise answering, converge later.
- CAP
- Under a network Partition you must choose Consistency or Availability. Brewer's theorem. Crucially it says nothing about the non-partitioned case.
- PACELC
- “If Partition, then A or C; Else, Latency or Consistency.” The extension that describes normal operation, which is where you actually spend your life.
- Quorum
- A majority of replicas. Systems that stay consistent under partition do so by requiring a majority to agree (Paxos, Raft) — which is exactly why the minority side goes unavailable.
- Sharding / partitioning
- Splitting data across nodes by some key so each node owns a slice. The only way to scale writes horizontally — and the source of the hot-key problem in Session 4.
Partitions are rare. Latency is constant. So the useful question is almost never “what happens under partition” — it is “what does a read cost me when everything is fine?”
| System shape | PACELC | What that means day to day |
|---|---|---|
| Single-region relational, sync replicas | PC/EC | Correct always; every write pays cross-zone acknowledgement (~1–3 ms). Reads from replicas may lag unless you force the primary. |
| Globally distributed strong SQL (Spanner-class) | PC/EC | Correct across continents; commit waits out clock uncertainty and cross-region quorum, so writes cost tens of ms. You buy correctness with latency, explicitly. |
| Multi-active NoSQL (Dynamo-style) | PA/EL | Single-digit-ms reads and writes anywhere; a read may return a value a few hundred ms out of date. Fine for carts and feeds, wrong for ledgers. |
| Tunable multi-model (Cosmos-style) | both, per request | You pick the level per operation, and the price list literally charges you differently for it. Strong reads cost roughly double the request units of eventual reads. |
A client in Frankfurt writes balance = 300
The write is accepted by the replica nearest to it. That replica now holds 300. The replicas in Virginia and Singapore still hold the old value, 500. Nothing is wrong yet — the write simply hasn't finished travelling. Physics: Frankfurt→Singapore is ~160 ms round trip at best.
A reader in Singapore asks for the balance, 1 ms later
The local replica answers immediately from its own copy: 500. Stale, and the reader has no idea. This is the deal you signed up for. It cost one local disk read and roughly 1 ms.
Correct choice when: a feed, a view count, a catalogue, a recommendation. Wrong choice when: the number is money, seats, or stock.
Same reader, same moment, but the system is configured strong
The Singapore replica cannot answer alone. It must confirm with a quorum — which means a round trip toward the leader. The reader gets 300, correctly, after roughly 80–160 ms.
You did not eliminate the trade-off. You moved it from “possibly wrong” to “definitely slow”. That is the entire content of the “else” branch of PACELC.
250 ms later, everyone agrees
The “eventually” in eventual consistency is usually milliseconds, not minutes. The engineering question is never “is it eventually consistent?” — it is “what is the p99 replication lag, and what does a user see inside that window?” Design for the window: read-your-own-writes via session tokens, or route a user's reads to the region that took their write.
Frankfurt is cut off from the other two
AP system: Frankfurt keeps accepting writes; so does the Virginia/Singapore side. When the link heals you have two divergent histories and must reconcile — last-writer-wins silently discards one, which is how “my order vanished” bugs happen.
CP system: Frankfurt is in the minority, so it refuses writes and returns errors until the partition heals. Nothing is lost; some users are down. The majority side carries on.
Neither is “safer”. Choosing AP means choosing to write reconciliation logic. Choosing CP means choosing to write graceful-degradation logic. If you write neither, you've chosen by accident.
OLTP (online transaction processing) is a shop till: thousands of tiny, precise operations per second, each touching one or two rows, each of which must be right. OLAP (online analytical processing) is the end-of-quarter review: one enormous question — “average basket size by region by week for three years” — that reads two columns out of a billion rows. Same data; opposite physics.
Two rules that will save you repeatedly:
- Never point your application's request path at a warehouse. Warehouse queries are billed and engineered for seconds-to-minutes and scanned bytes. A dashboard that refreshes per page-view against a columnar engine is a bill and a latency problem at the same time.
- Never run big analytics on your OLTP primary. A single unindexed aggregate can evict the working set from cache and take the whole transactional workload down with it. Route it to a read replica at minimum; export it to the warehouse properly.
The modern hybrid worth knowing: HTAP-ish features — a columnar cache or engine attached to the transactional database (Spanner's columnar engine, AlloyDB's columnar engine, Cosmos DB analytical store / Fabric mirroring) — that let you run analytics on operational data without an ETL hop. Useful, but they narrow the gap rather than close it.
-
Is this a copy of data that lives authoritatively somewhere else, read far more than written, where being a few seconds stale is fine and losing it entirely is survivable?yes →Cache (in-memory key-value). Put a TTL on it. Never make it the only copy.no ↓
-
Must several records change together, all-or-nothing — money moving, stock decrementing, a booking?yes →Relational. Multi-record ACID transactions with foreign keys are what it is for. If it must also be global and strongly consistent, that's the Spanner / Aurora DSQL class in Session 3.no ↓
-
Will you query it in ways you cannot fully predict today — ad-hoc joins, new filters, a reporting layer you haven't designed yet?yes →Relational. A schema plus a query planner is precisely the tool for unknown future questions. NoSQL trades that flexibility away for scale.no ↓
-
Is the access pattern “fetch or replace one self-contained record by its id”, with a shape that varies between records?yes →Document. Single-digit-ms at any scale, flexible schema. You must know your access patterns up front — the key design is the schema.no ↓
-
Is it a firehose of time-ordered or entity-ordered events at very high write volume, always read as a contiguous range of one sort order?yes →Wide-column. Petabytes, millisecond writes, one physical sort order — so the row key is the whole design. Telemetry, metrics, feature stores.no ↓
-
Is the primary question “how are these entities connected”, several hops deep?yes →Graph. Fraud rings, recommendations, permissions. Increasingly a mode of a general database rather than a separate product.no ↓
-
Is it a large volume of historical records you scan and aggregate, but rarely update?yes →Analytics / warehouse. Columnar. Keep it strictly off the request path.no ↓
Object storage is priced along three independent dimensions, and tiering trades them against each other: storage (per GB-month), requests (per thousand operations), and retrieval (per GB read back, on cold classes only). Colder classes cut dimension one and inflate dimension three — plus they add a minimum storage duration you pay for whether or not the object survives that long.
Hot / Standard — accessed daily
Highest per-GB price, no retrieval fee, no minimum duration, millisecond access. Everything is born here. Live model weights, active datasets, anything a user or a GPU is waiting on.
Cool / Infrequent access — roughly monthly
Storage drops by around half; a modest per-GB retrieval fee appears; a ~30-day minimum duration appears. Still millisecond access — nothing changes operationally, only the bill. This tier is nearly free money for last quarter's logs.
Cold / instant-retrieval archive — roughly quarterly
Storage drops another large step; retrieval fees rise sharply; minimum duration extends to ~90 days. Access is still immediate on the instant-retrieval variants. The breakeven maths matters here: if you read the data more than about once every five or six weeks, the retrieval fees exceed the storage saving.
Archive / deep archive — yearly or never
An order of magnitude cheaper again — around a dollar per TB-month at the bottom. In exchange: you must issue a restore job and wait minutes to hours before you can read anything, and the minimum duration is 180 days (AWS/Azure) or 365 days (Google Cloud). Compliance archives, tape replacement, seven-year retention.
Expiration — the rule most people forget to write
The single highest-leverage lifecycle rule is not a transition, it is a deletion. Data with no retention requirement and no reader should expire. Also expire incomplete multipart uploads — abandoned upload parts are invisible in the console, count as stored bytes, and quietly bill forever.
Minimum duration turns a saving into a penalty
Classic own-goal: a team lifecycles nightly backups straight to a 90-day-minimum archive class, while their retention policy deletes them after 30 days. Every single object is billed for 90 days of storage. They are paying triple the archive rate and believe they are saving 80%.
The rule: a transition only pays off if the object's expected lifetime exceeds the destination class's minimum duration, and the expected number of reads is below the breakeven. Two conditions. Check both, in that order.
Mechanically, the rule is declarative and lives on the bucket — this is the one place a config snippet is the mechanism:
# vendor-neutral shape of every lifecycle policy on every cloud rules: - filter: { prefix: "training-logs/" } # scope transitions: - { after_days: 30, to: "cool" } # age → colder class - { after_days: 120, to: "archive" } expire_after_days: 2555 # 7 years, then gone abort_incomplete_multipart_after_days: 7 # the forgotten one
Learn this table once and Sessions 2–4 become vocabulary drills. Every row is the same idea in three dialects.
| Neutral concept | AWS | Azure | Google Cloud |
|---|---|---|---|
| Object storage | Amazon S3 | Blob Storage (in a Storage account) | Cloud Storage (GCS) |
| Block storage | Amazon EBS | Azure Managed Disks | Hyperdisk (Persistent Disk on older series) |
| Shared file storage | Amazon EFS (NFS) · FSx (Windows/Lustre/ONTAP) | Azure Files (SMB+NFS) · NetApp Files | Filestore · Managed Lustre |
| Managed relational | RDS (MySQL/Postgres/SQL Server/Oracle/MariaDB) | Azure SQL Database · Azure Database for PostgreSQL / MySQL flexible server | Cloud SQL · AlloyDB (Postgres, HTAP) |
| Cloud-native relational | Aurora (MySQL/Postgres-compatible) | Azure SQL Hyperscale | AlloyDB |
| Global strongly-consistent SQL | Aurora DSQL | (no direct equivalent — closest is Cosmos DB strong + failover groups) | Spanner |
| Document / key-value NoSQL | DynamoDB | Cosmos DB (NoSQL API) | Firestore |
| Wide-column | Keyspaces (Cassandra) | Cosmos DB (Cassandra API) | Bigtable |
| In-memory cache | ElastiCache (Redis OSS / Valkey / Memcached) · MemoryDB | Azure Managed Redis | Memorystore (Valkey / Redis / Memcached) |
| Warehouse (OLAP) | Redshift · Athena (query-in-place) | Microsoft Fabric (Synapse in maintenance) | BigQuery |
| Zone-redundant storage | default for S3 Standard; Multi-AZ for RDS | ZRS / GZRS on the storage account | default for regional buckets; regional/HA disks |
Worked example · 1 TB for a year — hot, or archived?
Neutral arithmetic, using representative 2026 US-region list prices. A media company has 1 TB (1,024 GB) of finished renders. They must keep it 12 months. They expect to pull back 50 GB once during the year for a re-edit.
Access: milliseconds, any time. Operationally invisible.
(2,000 objects × $0.05/1,000)$0.10
Saving: $247.69/year per TB — 88%. At 500 TB that is ~$124k a year.
The check, every time: is expected object lifetime > destination minimum duration? If not, stop at the previous tier.
Three real, current examples
“$1/TB to store, $20K to retrieve”
A 2026 analysis works the numbers on a full petabyte in Deep Archive: about $1,013/month to keep — genuinely astonishing — but restoring the whole petabyte runs roughly $21K in retrieval fees plus ~$52K of egress, and takes 48 hours on the bulk tier. The moral is not “don't archive”; it is that a DR tier whose full restore takes 48 hours cannot satisfy a 12-hour RTO, and the restore cost belongs in the DR plan, not the storage plan.
Google re-priced its tiers in 2026
Multi-region Nearline went up (about $0.010 → $0.015 per GB-month) while multi-region Archive came down in US/EU (about $0.004 → $0.0024). Concretely: lifecycle policies written before 2026 may now be transitioning data into a class that is no longer the cheapest fit. Storage-class economics are not a “decide once” problem — put a calendar reminder on your tiering rules.
Well-Architected says the same thing
All three clouds publish a “performance efficiency” / “cost optimisation” pillar whose storage advice reduces to this session's first tree: select the storage service that matches the access pattern, then continuously re-evaluate as the pattern changes. The re-evaluation half is the half teams skip — data gets colder on its own; storage classes do not.
Mapping this onto what you already run
| Thing you already have | Its access shape | The class it belongs in |
|---|---|---|
| Modal Volume caching Gemma weights | Write once, read many, whole-file, read by every cold-starting replica | Object, hot — this is object storage with a filesystem face on it. Never tier weights cold: a cold-start that has to restore from archive is an outage. |
| Training / fine-tuning dataset | Sequential, high-throughput, read repeatedly across epochs | Object, hot for the active dataset; cool for previous versions you might reproduce from |
| Training checkpoints mid-run | Large sequential writes, frequent, latency-sensitive because GPUs idle during them | Block throughput or a low-latency object tier — this is the one place in your stack where MB/s, not IOPS, is the number to provision |
| Finished checkpoints after the run | Write once, read approximately never, but you dare not delete them | Archive — the reference text case, provided you keep them past the minimum duration (you will) |
| Inference request / response logs | Append-only firehose; queried later in aggregate for evals | Object then a warehouse — OLAP, never OLTP |
| Pipeline state: job records, model registry, eval runs, users | Small records, relational, must be correct, queried in ways you'll invent later | A managed relational database. Default-tree answer, and it's the right one. Session 3. |
Lab S1 — the taxonomy audit (no cloud resources, no bill)
This session is concepts-only, so the lab is deliberately paper. Take one system you already run — the vLLM-on-Modal deployment is ideal — and write out the table above for your own artefacts:
- List every persistent artefact. Weights, tokeniser, dataset, checkpoints, logs, config, secrets, metrics.
- For each, write one line: unit of access · read:write ratio · reads per month · who reads it · what breaks if it's 5 seconds stale · what breaks if it's gone.
- Run each one down the block/file/object tree, then the database tree. Write down where it exits.
- Circle every artefact where the answer differs from where it lives today. That circled list is your Session 2 and 3 to-do list.
Do this before Session 2 and the service names in S2 will land on hooks that already exist.
You now have the shapes and the axes. Session 2 puts real names, real classes and real 2026 prices on the top row of the taxonomy — object, block and file across all three clouds — and answers the question you'll ask on every project: exactly which class do my model weights go in, and what does the wrong answer cost?
Same three shapes. Three dialects. And the price list is where the design actually gets decided.
Session 1 said object, block, file. This session says which product, which class inside that product, and what the class costs you when you read the data back. That last clause is the whole game: a storage class is a bet about future access frequency, and the cheaper the bet, the harder the penalty for being wrong.
S3 is a bucket per namespace, a key per object, and a class per object that you can change at any time — by hand, by lifecycle rule, or automatically. Everything else in S3 is a feature bolted to that: versioning keeps old copies, replication copies to another bucket, and the classes are the temperature dial from Session 1.
Source note: AWS guidance covers S3, EBS and Glacier at catalogue depth and doesn't enumerate the classes. From here on this section is AWS documentation and current pricing.
| S3 class | $/GB-mo | Retrieval | Min duration | First byte | Reach for it when… |
|---|---|---|---|---|---|
| Standard | 0.023 | — | none | ms | Anything active. The default, and correct far more often than cost-optimisers admit. |
| Express One Zone | 0.11 | — | 1 h | single-digit ms | Latency-critical, high-request workloads co-located in one AZ — ML training loops over millions of small files. ~10× faster access, ~80% cheaper requests, 5× the storage price, and only one zone of durability. |
| Standard-IA | 0.0125 | $0.01/GB | 30 d | ms | Read about monthly. 128 KB minimum billable object size. |
| One Zone-IA | 0.01 | $0.01/GB | 30 d | ms | Only for data you can recreate — derived thumbnails, secondary copies. One AZ means an AZ loss is data loss. |
| Glacier Instant Retrieval | 0.004 | $0.03/GB | 90 d | ms | Quarterly access that must be instant when it happens: medical imaging, news archives, ML datasets you pull a few times a year. Breakeven vs Standard-IA is roughly one access every 5–6 weeks. |
| Glacier Flexible Retrieval | 0.0036 | $0.01 std / free bulk | 90 d | 1 min – 12 h | Backups you restore only in anger. Expedited (1–5 min) is best-effort unless you buy Provisioned Capacity Units at ~$100/unit/month. |
| Glacier Deep Archive | 0.00099 | $0.02 std / $0.0025 bulk | 180 d | 12–48 h | Tape replacement. 7–10 year compliance retention. ~$1 per TB-month. |
| Intelligent-Tiering | auto | none | none | ms (top 3 tiers) | Unknown or changing access patterns. Monitors each object and moves it between Frequent ($0.023) → Infrequent ($0.0125, 30 d) → Archive Instant ($0.004, 90 d) plus two opt-in deep tiers. Pays a small per-object monitoring fee, but no retrieval fees and no transition charges. |
The lifecycle rule is the mechanism, so here it is at S3's actual API shape:
// S3 lifecycle configuration — the config IS the mechanism { "Rules": [{ "ID": "logs-age-out", "Status": "Enabled", "Filter": { "Prefix": "inference-logs/" }, "Transitions": [ { "Days": 30, "StorageClass": "STANDARD_IA" }, { "Days": 180, "StorageClass": "DEEP_ARCHIVE" }], "Expiration": { "Days": 2555 }, "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } }]}
Storage is often not the biggest line. The four others:
- Requests. PUTs cost roughly 10× GETs, and archive-class PUTs cost more again (~$0.05/1,000). A pipeline writing millions of tiny objects pays more in requests than in bytes.
- Retrieval. Only on IA and colder — but at $0.03/GB (Glacier Instant), reading 10 TB back costs $300 on top of everything else.
- Egress. Data out to the internet is ~$0.09/GB for the first 10 TB, falling in tiers. Serving public assets straight from a bucket instead of through a CDN is a classic unforced error — CloudFront-to-origin transfer is free.
- Minimum billable object size. IA classes bill a 128 KB floor per object; archive classes add ~40 KB metadata. Ten million 20 KB objects in Standard-IA bill as if they were 128 KB each.
NoncurrentVersionExpiration rule at the same time.Redundancy Rosetta stone — the one table Azure candidates always lose marks on
| Protection level | AWS S3 | Azure Storage account | Cloud Storage |
|---|---|---|---|
| One datacentre only | One Zone-IA / Express One Zone | LRS — 3 copies, one datacentre | (no single-zone bucket except Rapid Bucket) |
| Across zones, one region | Standard and most classes (≥3 AZs) | ZRS — 3 zones, synchronous | Regional bucket |
| Two regions, async | Cross-Region Replication (you configure it) | GRS — async to a paired region, not readable until failover | Dual-region bucket |
| Two regions, readable secondary | CRR + read the second bucket yourself | RA-GRS — secondary readable at any time | Dual-region (both readable) |
| Zones + regions | Standard + CRR | GZRS / RA-GZRS — ZRS in primary, async to secondary | Multi-region bucket |
-
Is a GPU, a user, or a request path waiting on this read?yes →Hot / Standard. If it's also latency-critical, high-request and single-AZ-tolerant, consider S3 Express One Zone / GCS Rapid. Do not tier this data. Ever.no ↓
-
Is the access pattern genuinely unknown, or does it change over time?yes →Intelligent-Tiering (AWS) / Autoclass (GCS) / lifecycle-with-last-access-tracking (Azure). No retrieval fees, no early-deletion charges — you buy away the risk of guessing wrong for a small monitoring fee.no ↓
-
Will this object be deleted or overwritten before the destination class's minimum duration (30 / 90 / 180 / 365 days)?yes →Stay hot. The early-deletion penalty exceeds the saving. This is the single most common tiering mistake — check it before anything else.no ↓
-
Read roughly monthly, and must be instant when read?yes →Standard-IA / Cool / Nearline. Watch the 128 KB minimum billable size if your objects are small.no ↓
-
Read roughly quarterly or less, but a wait of hours would be unacceptable when it happens?yes →Glacier Instant Retrieval / Cold / Coldline. Cheap to hold, dear to read — model the read volume, not just the GB.no ↓
-
Read once a year at most, and you can wait hours, and you'll keep it past 180 days (365 on GCS)?yes →Deep Archive / Archive. Now put the full-restore cost and time in your DR runbook, because it is not free and it is not fast.no ↓
-
Does anyone actually need this data at all?yes →Then it's in one of the tiers above.no ↓
You are renting a disk over the network and pretending it's local. Two dials: how big and how fast. The generational shift across all three clouds has been decoupling those two dials — older volume types tied speed to size (want more IOPS? buy more GB you don't need); modern ones let you provision capacity and performance independently.
-
Is the data pure scratch — shuffle spill, a re-downloadable weight cache, temp files — that you can lose on reboot?yes →Local/ephemeral NVMe. Fastest thing available, and it's often already included in the instance price.no ↓
-
Do many instances need to read the same large immutable blob concurrently at high throughput?yes →On GCP, Hyperdisk ML (read-only, multi-attach). Elsewhere, this is usually a file-storage or object-storage question, not a block one.no ↓
-
Is the I/O large and sequential — logs, batch scans, media — with low IOPS need and a big capacity bill?yes →HDD-class: st1 / Standard HDD / Hyperdisk Throughput. Roughly half the per-GB price of SSD.no ↓
-
Have you measured a need for more than ~16,000 IOPS, sub-millisecond latency, or five-nines volume durability?yes →io2 Block Express / Ultra Disk / Hyperdisk Extreme. Measured, not anticipated — this tier is several times the price.no ↓
-
Does the volume need to survive the loss of an entire zone without a restore?yes →Hyperdisk Balanced High Availability (GCP) or zone-redundant equivalents; on AWS the pattern is application-level (Multi-AZ database) rather than a cross-AZ volume.no ↓
You reach for file storage for one of two honest reasons: several machines must share a mutable working directory, or software you didn't write demands a POSIX path. It is the most expensive of the three shapes per GB, so use it deliberately.
Worked example · 40 TB of inference logs, three ways
Your service writes 40 TB/year of request/response logs as ~4 million objects averaging 10 MB. You re-read about 2% of them (800 GB) during eval work in the first three months, and essentially nothing after that. Compliance says keep 7 years. US region, S3 list prices.
Read the result honestly: hand-tuned lifecycle (B) beats Intelligent-Tiering (C) by about $1,200 in year one because the access pattern is genuinely predictable. If eval work were sporadic and unpredictable, C would win on retrieval fees alone — and C never gets the minimum-duration decision wrong. Choose B when you know the pattern; choose C when you're guessing.
Three real, current examples
AWS's own numbers on checkpoint storage
AWS's storage blog works a large-training example: loading 125 TB of checkpoint data in two minutes requires roughly 1,042 GB/s — about 8.3 Tbps of aggregate throughput. Their guidance is a tiered pattern — S3 (optionally Intelligent-Tiering) as the durable home, S3 Express One Zone or FSx for Lustre as the hot path — plus retries with exponential backoff and connection-pool tuning, because thousands of ranks checkpointing simultaneously create demand spikes that hit service limits regardless of which storage you chose.
FUSE and caches change the calculus
Google reports Cloud Storage FUSE delivering up to 2.2× faster training time and 2.9× higher throughput versus native framework dataloaders, and Rapid Cache (formerly Anywhere Cache) reaching multi-Tbps aggregate reads with reduced GPU blocked time and much faster checkpoint restores. Practical consequence: the reflex “ML training needs a parallel filesystem” is weaker than it was two years ago. Benchmark object-plus-cache before you provision Lustre or Filestore.
Neither provider is universally cheaper
A 2026 cross-cloud comparison finds request charges, egress and retrieval fees routinely add 30–70% on top of the theoretical per-GB storage bill, and that AWS vs Azure vs GCP flips depending on object size, access pattern and egress volume. Take the practical lesson rather than a winner: model all five dimensions — storage, requests, retrieval, egress, minimum durations — or your comparison is fiction.
Your Modal Volume, translated to each cloud
A Modal Volume caching Gemma weights is doing exactly one job: make a large immutable blob available to every cold-starting replica fast enough that the cold start isn't dominated by the download. That is a distributed read cache in front of object storage. Here is the same job, natively, on each cloud:
| Layer | AWS | Azure | Google Cloud |
|---|---|---|---|
| Durable home for weights | S3 Standard (not IA — cold starts can't wait) | Blob Hot | GCS Standard, regional, co-located with the GPUs |
| Fast read path for many replicas | S3 Express One Zone directory bucket, or Mountpoint-for-S3 CSI driver on EKS | BlobFuse2 with local cache on the node | Hyperdisk ML (read-only, multi-attach) or Cloud Storage FUSE + Rapid Cache |
| Node-local cache | Instance-store NVMe | Ephemeral OS/temp disk | Local SSD |
| Training dataset | S3 + FSx for Lustre if you need POSIX at speed | ADLS Gen2 + Managed Lustre | GCS + Managed Lustre, or GCS FUSE for most cases |
| Checkpoints during a run | S3 Express One Zone or FSx Lustre; gp3/io2 if writing locally first | Premium SSD v2 then async copy to Blob | Hyperdisk Balanced (provision throughput, not IOPS) then GCS |
| Finished checkpoints | Deep Archive after 180+ days | Archive after 180+ days | Archive — remember 365-day minimum |
- Cold-start latency is a storage problem. A 9B model in bf16 is ~18 GB. At 1 GB/s that's 18 seconds of GPU sitting idle at $2–10/hour before a single token is served. This is why weight buckets stay hot and why read caches earn their keep.
- Checkpoint writes want MB/s, not IOPS. Nearly every default block volume is tuned for random IOPS. A checkpoint is one enormous sequential write. On gp3 and Hyperdisk Balanced you can turn the throughput dial independently — do that, and leave IOPS at baseline.
- Small-file datasets destroy object-storage economics. Ten million 30 KB training images = 10M requests per epoch plus per-object overhead. Shard into WebDataset/TFRecord-style archives of 100 MB–1 GB before you do anything else.
- Your Vertex AI T4/L4 autoscaling script is a cold-start machine. Scaling 1→3 means two nodes pulling the full weight set on every scale-out event. Where the weights live determines whether that's 20 seconds or 4 minutes.
Lab S2 — lifecycle a bucket and watch the class change
Pick one cloud. Free tier / smallest SKU only. Set a budget alert before you create anything.
- Budget alert first. AWS Budgets / Azure Cost Management budget / GCP Billing budget — set it to $5 with an email at 50%. Do this even for a $0.05 lab; the habit is the point.
- Create one bucket in your nearest region. Default settings. Turn on versioning so you can see the noncurrent-version trap for yourself.
- Upload three small files (a few KB each) under the prefix
lab/. - Add a lifecycle rule scoped to
lab/: transition to the cool tier after 30 days, expire after 60, and abort incomplete multipart uploads after 7 days. Add a noncurrent-version expiration of 7 days. - Manually change one object's storage class to the cool tier in the console and note the timestamp — this is what starts a minimum-duration clock.
- Now delete that one object and look at the cost explorer line item over the next two days. You will see an early-deletion charge for the remaining days. That single sub-cent line item is the most valuable thing in this lab — it makes the penalty concrete.
- Open the pricing calculator for the same cloud and price 100 TB in each class for 3 years. Compare against your S1 arithmetic.
- Empty the bucket, including all object versions and delete markers. With versioning on, deleting an object does not free the bytes — the previous version is still stored and still billed. Use “empty bucket” / “delete all versions”, not a normal delete.
- Delete the bucket itself.
- Check for incomplete multipart uploads before deleting — abandoned parts are invisible in the object list and bill indefinitely.
- Accept the early-deletion charge. You will be billed the remainder of the 30-day minimum on that one tiered object. It will be well under a cent. Leaving it in place to avoid the fee would cost more.
- If you created any disk or file share while exploring: delete the volume, not just the VM. Block volumes bill from creation to deletion regardless of attachment, and this is the single most common orphaned resource on every cloud.
- Leave the budget alert in place. It costs nothing and it will save you later.
Raw storage is now placed on all three clouds. Session 3 moves up one layer to the engines that add concurrency control and query on top of it — managed relational databases — and to the decision that costs the most to reverse: regional, multi-region, or genuinely global.
Almost every system has one relational database at its centre. Getting it wrong is the most expensive mistake in this stage to undo.
You can change an object's storage class with an API call. You can attach a bigger disk in an afternoon. Changing the database at the heart of a system means a schema migration, a data migration, a dual-write period and a cutover — months, not minutes. So this is the decision that deserves the most deliberate thought, and the one where “boring and correct” pays the largest dividend.
Renting a flat versus owning a house. The landlord fixes the boiler, replaces the roof and re-paints the exterior (patching, backups, failover). You still choose the furniture and you're still responsible for the mess you make inside (schema, indexes, queries, connection handling). And there are things you're simply not allowed to do — no knocking through walls: no OS shell, no arbitrary extensions, no superuser.
1 · Engine and version
PostgreSQL, MySQL, SQL Server, Oracle, MariaDB. Pick the engine your team knows. Version matters more than people expect — managed services force major-version upgrades on a schedule, and the extensions available are a curated list, not whatever's on PGXN.
2 · Compute size
vCPU and memory, either as an instance class or as an abstract unit (Azure DTUs/vCores, Aurora Capacity Units, Spanner processing units). Memory is usually the binding constraint, because it determines how much of your working set stays in the buffer cache.
3 · Storage and IOPS
Everything from Session 2 applies — this is block storage underneath. Cloud-native engines (Aurora, Hyperscale, AlloyDB) hide it: storage auto-grows and is replicated many ways across zones, and you're billed for what you use plus I/O.
4 · Availability topology
Single instance, zone-redundant standby, read replicas, cross-region replicas. This is the dial that actually determines your SLA, your RPO and roughly half your bill.
- Primary / writer
- The one node that accepts writes. In classic relational architecture there is exactly one, and that is the fundamental scaling limit.
- Standby
- A synchronously-replicated copy you cannot query. It exists purely to be promoted. Doubles your cost and buys you automatic failover.
- Read replica
- An asynchronously-replicated copy you can query. Scales reads; lags the primary by milliseconds to seconds.
- Failover
- Promoting a standby to primary. Automatic in managed services; typically 30–120 seconds on instance-based services, single-digit seconds on cloud-native ones.
- PITR
- Point-in-time recovery — restore to any second inside the retention window, using a base backup plus the transaction log. The real protection against “someone ran the wrong UPDATE”.
- Connection pooling
- Reusing a small set of database connections across many clients. Relational databases handle a few hundred connections well and thousands badly; serverless compute makes this worse, which is why RDS Proxy, PgBouncer and Azure SQL's built-in pooling exist.
Managed is right almost always. The genuine exceptions:
- You need an extension or a version the service doesn't offer. Common with exotic Postgres extensions, or engine builds pinned by a vendor application.
- You need OS-level access — a custom collation, a filesystem-level tool, an agent that must run on the DB host.
- Regulatory or data-residency constraints the managed service doesn't satisfy in your region.
- Cost at very large, very steady scale, where a well-run self-managed cluster on reserved instances genuinely undercuts the managed premium — and you have the DBA capacity to run it properly. Be honest about the second half.
Everything else — “we want control”, “we already know how to run Postgres”, “managed is expensive” — is usually a decision to spend engineer-hours on undifferentiated work. The patterns catalog calls the managed option Database-as-a-Service and treats it as the default for cloud-native applications for exactly this reason.
A restaurant with one kitchen. Adding more waiters (read replicas) serves more customers reading the menu and collecting food, and it's cheap. But every order still passes through one kitchen. To cook more you either build a bigger kitchen (vertical scaling — finite) or open separate kitchens each responsible for different dishes (sharding — complex) or use a fundamentally different design where every branch can cook independently and they reconcile (distributed SQL).
Steady state
Primary in zone A, synchronous standby in zone B, two read replicas. Application connects to a DNS endpoint, not an IP — that indirection is the entire failover mechanism.
Zone A loses power
The primary is gone mid-transaction. Because replication to the standby was synchronous, every committed transaction is already durable in zone B. RPO = 0. In-flight uncommitted transactions are lost, correctly — they were never acknowledged.
Detection (~10–30 s)
The control plane's health checks fail repeatedly. Deliberately not instant — a single missed heartbeat must not trigger a failover, or a network blip becomes an outage. This delay is the price of not flapping.
Promotion and DNS swap (~10–60 s)
Standby is promoted to primary; the endpoint's DNS record is repointed. Your application's cached DNS and open connection pool are now the long pole. Set a short DNS TTL, and make sure your driver actually reconnects rather than holding dead sockets — this is where most “the failover took 15 minutes” stories actually come from.
From the application's point of view
Roughly 30–120 seconds of write errors (single-digit seconds on Aurora-class engines), then normal service. Reads from replicas continued throughout if you routed them separately.
Two design consequences, both yours not the cloud's: (1) retry with exponential backoff and jitter on transient database errors, and (2) test it — every managed service has a “force failover” button. Press it in staging, during working hours, and measure.
Compare harder — the capability grid you'll actually be asked about
| Capability | AWS | Azure | Google Cloud |
|---|---|---|---|
| In-region HA | RDS Multi-AZ (instance = unreadable standby; DB cluster = 2 readable standbys). Aurora: 6 storage copies across 3 AZs by design. | Zone-redundant configuration; Business Critical keeps always-on replicas on local SSD, one of which is readable. | Cloud SQL regional HA (unreadable standby in a second zone). AlloyDB is zone-redundant by architecture. |
| Typical failover | RDS 60–120 s · Aurora <30 s, often single-digit | Azure SQL <30 s typical | Cloud SQL ~60 s · AlloyDB faster |
| Readable replicas | Up to 15 (Aurora replicas share storage, so they add no write overhead) | Geo-replicas / named replicas; Hyperscale adds up to 30 high-availability + named replicas | Cloud SQL read replicas incl. cross-region; AlloyDB read pools |
| Backups & PITR | Automated backups, PITR to the second within retention (up to 35 days), snapshots survive instance deletion | Automatic backups with PITR (1–35 days) plus long-term retention up to 10 years | Automated backups + PITR via write-ahead-log retention |
| Storage ceiling | RDS 64 TiB · Aurora 128 TiB | General Purpose 4 TB · Hyperscale 128 TB | Cloud SQL 64 TB · Spanner effectively unbounded |
| Scale-to-zero / pause | Aurora Serverless v2 → 0 ACUs; RDS instances can be stopped for up to 7 days | Azure SQL serverless auto-pause; flexible server stop/start | Stop the Cloud SQL instance manually (storage still bills) |
| Cross-region | Aurora Global Database (sub-second lag, write forwarding); cross-region read replicas | Failover groups & active geo-replication (async, readable secondary) | Cross-region read replicas; Spanner multi-region configurations |
| Global strong consistency | Aurora DSQL | — (Cosmos DB, not Azure SQL) | Spanner |
| Connection pooling | RDS Proxy (billed per ACU/vCPU of the database) | Built into the Azure SQL gateway; PgBouncer available on flexible server | Cloud SQL Auth Proxy handles auth & encryption; PgBouncer for pooling |
| Auth without passwords | IAM database authentication | Microsoft Entra ID authentication | IAM database authentication |
Reading it as an architect, not a memoriser: the rows split into two groups. Rows 1–4 are resilience — every cloud sells essentially the same thing at essentially the same price, so they are rarely the deciding factor. Rows 5–8 are ceilings — how far the design goes before it needs replacing. Choose on the ceilings, because those are the ones that force a migration later, and a migration is the expensive event this session exists to help you avoid.
-
Do you need an engine feature the PaaS offering forbids — an unsupported extension, SQL Agent, cross-database queries, OS access?yes →Managed Instance (Azure SQL MI) or, failing that, self-managed on a VM. Accept the toil deliberately.no ↓
-
Do writers in multiple regions need to write the same rows with strong consistency and zero RPO?yes →Spanner (GCP) or Aurora DSQL (AWS). Budget for tens of milliseconds of commit latency and a narrower SQL surface. Be certain you need this — it is the most expensive answer on the tree.no ↓
-
Is the workload idle most of the time — dev, test, internal tooling, a weekly batch?yes →Aurora Serverless v2 with a 0-ACU floor, Azure SQL serverless with auto-pause, or Cloud SQL you stop when unused. Remember storage still bills at zero compute.no ↓
-
Do you need >64 TB, near-instant restore of a very large database, or read replicas that add no write overhead?yes →The cloud-native tier: Aurora, Azure SQL Hyperscale, or AlloyDB. Storage and compute separate, replicas share storage.no ↓
-
Is read traffic the bottleneck, with writes comfortably inside one instance?yes →Keep the engine; add read replicas and route reporting and read-only paths to them. Cheapest real scaling move there is.no ↓
-
Is write throughput the bottleneck, measured, after indexing and batching work?yes →Vertical scale first (it's one restart), then shard by tenant or entity, then distributed SQL. In that order — each step costs an order of magnitude more effort.no ↓
-
Is there a hard regulatory requirement that data stay in one jurisdiction?yes →Single region, and the conversation is over. Add zone redundancy inside it.no ↓
-
Would losing the last few seconds of writes in a regional disaster be acceptable?yes →Cross-region async replica — Aurora Global Database, Azure failover groups, Cloud SQL cross-region replica. RPO seconds, RTO minutes, cheap, and writes stay fast because they never wait for the far region.no ↓
-
Do users on other continents need low-latency reads only, with writes still funnelled to one place?yes →Global read replicas with write-forwarding. Local reads are fast; writes pay the round trip to the home region. Covers a surprising share of “we need a global database” requests.no ↓
-
Must users on multiple continents write the same records, with every reader seeing the latest value, and RPO exactly zero?yes →Globally distributed strong SQL: Spanner or Aurora DSQL. Genuinely remarkable engineering; you pay in commit latency and money.no ↓
-
Must they write concurrently but can you tolerate conflict resolution and brief divergence?yes →Multi-active NoSQL — DynamoDB global tables, Cosmos DB multi-region writes. Session 4. Cheaper and faster than global SQL, and you own the reconciliation logic.no ↓
Worked example · when does a read replica beat a bigger primary?
A Postgres workload on a managed 8-vCPU / 32 GB instance is at 85% CPU. Traffic is 90% reads. Two options; representative on-demand pricing, one US region.
Verdict: at this size the two cost almost the same, and vertical scaling wins because it needs no application change and helps writes too. The maths flips at scale and at the edges:
- Replicas win when you've run out of instance sizes; when reads are geographically distant (a replica in-region beats a fast primary across an ocean); when you want reporting isolated so one bad analytical query can't take production down; or when read demand is spiky and you can add and remove replicas.
- Bigger primary wins when writes are also growing, when your application can't cleanly separate read and write connections, or when replica lag would break correctness.
- Neither wins if the real problem is a missing index or an N+1 query. Check that first — it is free, and it is the answer more often than either option above.
Second worked example · Aurora Serverless v2 vs provisioned — where's the break-even?
Aurora Serverless v2 bills roughly $0.12 per ACU-hour, where 1 ACU ≈ 2 GiB of memory plus matching CPU. A provisioned db.r6g.large (2 vCPU / 16 GiB) is roughly $0.29/hour on demand — about the capacity of 8 ACUs.
Reserved instances push provisioned further ahead again (40–72% off for commitments). So: Serverless v2 for dev, test, demos, internal tools and genuinely spiky production; provisioned for steady load. And in both cases Aurora storage at ~$0.10/GB-month keeps billing even at 0 ACUs — a “paused” 500 GB cluster is still $50/month. Deleting the cluster is the only way to stop that.
Three real, current examples
Target and True Digital on Spanner
Google's 2026 customer write-up describes Target choosing Spanner Graph for product recommendations, blending graph traversals with vector search over proprietary embeddings and shipping it in time for the 2025 Black Friday / Cyber Monday peak; and True Digital Group (Thailand) consolidating a fragmented multi-database search stack onto Spanner after inconsistent tokenisation and stale data drove users away from search entirely. The recurring theme is not “we needed global writes” — it is consolidation: one strongly-consistent engine replacing several specialised ones that had each hit a scale or consistency wall.
Analyst and TEI numbers, read carefully
Gartner ranked Google (Spanner) #1 in the Lightweight Transactions use case in its 2025 Critical Capabilities report for operational cloud DBMS — second consecutive year — and a Forrester Total Economic Impact study commissioned by Google modelled a composite organisation at 132% ROI with a nine-month payback. Worth knowing because these numbers appear in procurement decks. Worth discounting because a vendor-commissioned TEI models a composite, not your workload. Use them as evidence the category is credible, not as a forecast.
Aurora DSQL exists now, and it changes the AWS answer
Aurora DSQL previewed at re:Invent 2024 and reached GA on 27 May 2025, with active-active multi-region writes, multi-region strong consistency, 99.999% multi-region availability and PostgreSQL compatibility, billed in DPUs with no upfront commitment. Practical consequence for you: “global strongly-consistent SQL means moving to GCP” stopped being true in 2025. Practical caution: it is a different engine, not Aurora with new billing — evaluate the SQL surface against your schema before assuming a migration is a lift-and-shift.
Where a relational database fits in your inference pipeline
A GenAI serving stack usually has no obvious database — right up to the moment you need to answer “which model version served this request, and what did the eval say?”. Then you discover you've been keeping that state in log files, S3 keys and someone's spreadsheet. The relational database is the control plane of your inference platform:
| Entity | Why relational, specifically | Rough scale |
|---|---|---|
| Model registry — model, version, base checkpoint, quantisation, weight URI, status | Foreign keys to deployments and evals; you will invent new queries about it constantly | hundreds of rows |
| Deployments — endpoint, replica count, GPU type, autoscaling bounds, current model version | Must be transactionally consistent with the registry: a deployment pointing at a deleted model version is a live incident | tens of rows |
| Eval runs — dataset version, metrics, judge model, timestamp | Joins across model × dataset × metric are the whole point, and you cannot predict the cuts you'll want | thousands |
| Jobs / batch inference — status, retries, idempotency key | SELECT … FOR UPDATE SKIP LOCKED makes a managed Postgres a perfectly good job queue at your scale, and one fewer service to run | thousands/day |
| Per-request logs — prompt, response, tokens, latency | Not here. Firehose → object storage → warehouse. Putting these in the OLTP database is the single most common way teams accidentally destroy their control plane's performance | millions/day |
Concretely for your stack: Cloud SQL for PostgreSQL in the same region as your Vertex AI endpoints and L4/T4 nodes — one instance, HA enabled, PITR on, private IP so it's reachable over C4's private networking rather than a public endpoint, and Secret Manager for the credentials rather than environment variables. On AWS the identical design is RDS for PostgreSQL Multi-AZ. From GKE or Vertex, connect through the Cloud SQL Auth Proxy / IAM database authentication so there's no long-lived password at all.
pgvector. That makes “add embeddings to the control-plane database” tempting, and for a modest corpus it is genuinely the right call — one database, transactional consistency between a document and its embedding, no extra service. It stops being right somewhere in the millions-of-vectors range, where a purpose-built index earns its keep. That threshold conversation belongs to your RAG track; here, just note that the option exists and that the default answer for a small corpus is “use the database you already have”.Lab S3 — stand up the smallest possible managed Postgres, then fail it over
Budget alert first, smallest SKU only, and read the teardown before you start.
- Budget alert: $5 with an email trigger at 50%. Non-negotiable — a database is the easiest thing in this stage to leave running.
- Create the smallest managed PostgreSQL available:
db.t4g.micro(RDS), Burstable B1ms (Azure flexible server), or a shared-core Cloud SQL instance. Single zone to start. Public access off; connect from a small VM or Cloud Shell in the same network. - Create a two-table schema:
model_versionanddeployment, with a foreign key. Insert a handful of rows. - Run a transaction that updates both tables, and deliberately roll it back. Confirm neither change persisted — that's atomicity, which is the entire reason you chose this engine.
- Enable HA / Multi-AZ / zone redundancy. Note the price change in the console before you confirm.
- Press force failover. Time it with a stopwatch while a loop runs
SELECT 1every 200 ms. Write down the number of failed attempts. That number is your real RTO — not the one in the marketing page. - Take a snapshot, then do a point-in-time restore to five minutes ago into a new instance. This is the step everyone skips and the one that matters most.
- Delete the restored instance first, then the primary. A PITR restore creates a whole second database that bills exactly like the first — this is the most expensive thing in this lab.
- Turn off “retain automated backups” / delete manual snapshots. Snapshots survive instance deletion by design and keep billing for storage. Deleting the instance is not enough.
- Delete read replicas before the primary if you created any — some services block primary deletion until replicas are gone.
- Check for orphaned block volumes and static IPs left behind by the helper VM, if you used one. Volumes bill from creation to deletion regardless of attachment.
- Remember: provisioned database compute bills per hour whether or not a single query arrives. An idle t4g.micro is a few dollars a month; an idle instance you sized “for the demo” at 8 vCPU is a few hundred. Check the size before you walk away, and prefer to delete rather than stop.
- Keep the budget alert.
Session 3 assumed the answer was “rows and transactions”. Session 4 covers everything you reach for when it isn't: document, key-value and wide-column stores, the caching tier in front of all of them, and the columnar warehouses at the end of the pipeline — plus the partition-key design that decides whether your NoSQL table scales or throttles.
NoSQL doesn't fail slowly. It works beautifully at 10× your test load and then throttles at 11× because of one line of schema you wrote in week two.
Relational databases forgive a bad schema — the query planner works around it, and you add an index later. NoSQL does not. In a distributed key-value or wide-column store, the key is the physical layout: it decides which machine holds the data, which means it decides whether load spreads or concentrates. Choose it before you know your access patterns and you will discover the mistake in production, at scale, on a Friday.
So this session is really two things: the shapes (document, key-value, wide-column, cache, columnar) and the one design skill that makes or breaks all of them — partitioning.
A filing box of self-contained forms. Each form carries everything about one thing — order, plus line items, plus address — so you fetch one form and you're done. Different forms can have different fields. No joins, because everything you need is already on the page.
A cloakroom. Ticket in, bag out. There is no “find me all the blue bags” — that question simply doesn't exist in the API. In exchange it is the fastest and cheapest operation in computing.
A library shelf sorted by one scheme. Sources are physically ordered by call number, so “everything from A340 to A390” is one sweep of the hand. Ask for “everything by this sources” and you're walking the whole library. One sort order, chosen once.
All three shapes distribute data the same way: hash or range the key, assign to a partition, put partitions on machines. The API you get is a direct consequence.
- Partition key
- The attribute hashed to choose a partition. Determines where data lives. Also called the hash key, shard key, or (Bigtable) the leading part of the row key.
- Sort key
- An optional second attribute that orders items within a partition. Partition key + sort key together form the primary key and enable efficient range queries.
- Cardinality
- How many distinct values a key has. High cardinality is what makes load spread;
tenant_idin a 5-tenant system is low cardinality and will bite you. - Hot key / hot partition
- One key or partition absorbing a disproportionate share of traffic and hitting its per-partition ceiling while the rest of the table idles.
- RCU / WCU · RRU / WRU
- DynamoDB's capacity units. 1 WCU = one 1 KB write per second; 1 RCU = one strongly-consistent 4 KB read per second, or two eventually-consistent ones. The RRU/WRU variants are the per-request (on-demand) equivalents.
- RU/s
- Cosmos DB's request unit per second — a single blended currency covering CPU, IOPS and memory. A 1 KB point read costs 1 RU; everything else is priced relative to that.
- GSI
- Global secondary index — a second copy of the table with a different partition key, so you can query by something other than the primary key. It has its own capacity and every base-table write also writes the index.
The schema is the mechanism here, so this is the one snippet that belongs in Pass 2:
# The whole design decision, in six lines. Access pattern first: # "get all inference jobs for one tenant, newest first" PartitionKey: "TENANT#{tenant_id}" # WHERE the data lives — spreads load SortKey: "JOB#{iso8601_ts}#{job_id}" # ORDER inside the partition # → query(PK = TENANT#42, SK begins_with "JOB#14201B-07") is ONE partition read. # → "all jobs across all tenants by status" is NOT answerable. Add a GSI, or # accept a full scan. Decide this now, not after launch.
- Indexes multiply writes. Every global secondary index is a second table maintained synchronously. Three GSIs means a single logical write costs roughly four writes. This is the most common reason a DynamoDB bill lands at 3× the estimate.
- Item size is a multiplier, not a footnote. A write unit covers 1 KB. A 3.5 KB item costs 4 write units — so a schema change that adds a verbose field to a hot table can quadruple the bill without a single extra request.
- Consistency is priced. A strongly-consistent read costs twice an eventually-consistent one, on DynamoDB and Cosmos DB alike. Defaulting the whole application to strong is a decision to double your read bill, usually for no user-visible benefit.
- Cross-region replication multiplies writes again. A global table writes in every replica region; three regions means roughly 3× the write charges plus cross-region transfer.
userid#reverse_timestamp is a good key and timestamp#userid is a catastrophic one — the latter sends every write to the same end of the keyspace, hotspotting one node. Salt or field-promote to spread it.A cache is a bet that you'll be asked the same question again soon. It's the notepad next to the phone: you write down the answer you just looked up, because looking it up again costs more than reading your own handwriting. And like a notepad, the danger isn't losing it — it's trusting it after the answer has changed.
- Is the underlying store already single-digit-millisecond and comfortably within capacity?yes →Don't cache. You'd add an invalidation bug surface and a new failure mode to save nothing. This exits right far more often than people expect.no ↓
- Would serving a value a few seconds stale cause a correctness problem — money, stock, permissions?yes →Don't cache it, or cache with explicit invalidation on write and a very short TTL, and accept that you now own a distributed-systems problem.no ↓
- Is the same key read many times between writes, and is the computation or query genuinely expensive?yes →Cache-aside with a jittered TTL and single-flight on miss. The standard, correct answer.no ↓
- Is it session state, a rate-limit counter, a leaderboard, or a job queue?yes →Redis/Valkey data structures — sorted sets, hashes, streams. This is cache infrastructure used as a database, and it's legitimate: use a durable variant (MemoryDB, or persistence enabled) if losing it matters.no ↓
Everything so far answers “what is the state of this one thing right now?”. The warehouse answers “what happened across everything, over time?”. Different question, different physics (columnar, from S1), different service, and — critically — a different billing model: you generally pay for bytes scanned or for reserved compute slots, not for rows stored.
SELECT * genuinely expensive and column pruning genuinely free. Partition and cluster your tables; the difference between a partitioned and unpartitioned scan is often two orders of magnitude on the invoice.Two numbers decide the warehouse billing model. On-demand / per-TB-scanned wins for sporadic, exploratory use — you pay nothing between queries. Capacity / slot / reservation wins once queries are frequent and predictable, and it also caps the blast radius of somebody's accidental unfiltered scan. The migration point usually arrives the month after a dashboard goes live and starts auto-refreshing every five minutes.
Session 1's tree, now with names on the exits. This is the artefact to keep.
| If the workload is… | Shape | AWS | Azure | Google Cloud |
|---|---|---|---|---|
| Transactions, joins, unpredictable queries | Relational | RDS · Aurora | Azure SQL DB · PG flexible server | Cloud SQL · AlloyDB |
| Global writes, strong consistency, RPO 0 | Distributed SQL | Aurora DSQL | Cosmos DB (strong) | Spanner |
| Fetch/replace one self-contained record by id, huge scale | Document / KV | DynamoDB | Cosmos DB (NoSQL API) | Firestore |
| High-volume time-ordered events, one sort order | Wide-column | Keyspaces · Timestream | Cosmos DB (Cassandra API) | Bigtable |
| Sub-millisecond reads of a derived copy | Cache | ElastiCache · MemoryDB · DAX | Azure Managed Redis | Memorystore (Valkey) |
| Scan-and-aggregate over history | Columnar OLAP | Redshift · Athena | Microsoft Fabric | BigQuery |
| Relationship traversal several hops deep | Graph | Neptune | Cosmos DB (Gremlin) | Spanner Graph |
| Nearest-neighbour over embeddings (RAG track) | Vector | OpenSearch · pgvector on RDS/Aurora · S3 Vectors | Cosmos DB DiskANN · AI Search | Vertex AI Vector Search · pgvector on AlloyDB · Spanner/BigQuery vector |
Worked example · on-demand vs provisioned at a real request rate
A metadata table for your inference platform: 500 writes/second and 2,000 reads/second sustained, average item 1.5 KB, reads eventually consistent, 100 GB stored. Current US pricing.
At a genuinely constant load, provisioned still wins by a wide margin — roughly 6.7×. But run the same table at a realistic duty cycle: a platform whose traffic is concentrated in an 8-hour working day and near-zero overnight consumes about a third of those request units, so on-demand falls to roughly $1,300/month while provisioned-for-peak stays at $595 only if auto-scaling tracks the curve well — and auto-scaling reacts in minutes, so you must provision above the peak to avoid throttling during ramps. Add weekend troughs and unpredictable eval bursts and the two converge fast.
The rule that survives the arithmetic: predictable and flat → provisioned (plus a Savings Plan). Spiky, unknown or bursty → on-demand. And the November 2024 price cut moved that boundary meaningfully toward on-demand, so recompute rather than reusing a 2023 heuristic.
PK = TENANT#{id}, and one enterprise tenant generates 60% of traffic. All of that tenant's writes hash to a single partition, which is capped at 1,000 write units per second regardless of table-level capacity. At 500 writes/s × 2 WRU × 60% = 600 write units on one partition you're fine; grow 2× and you throttle — while CloudWatch shows the table using a fraction of its provisioned capacity. Engineers spend a day raising table capacity, which changes nothing.
The fix is write sharding:
PK = TENANT#{id}#{shard 0–9}, writes pick a shard at random, reads query all ten and merge. Ten times the write ceiling for that tenant, at the cost of a fan-out read. Use CloudWatch Contributor Insights to find the offending keys — it is built exactly for this and almost nobody turns it on.
Three real, current examples
“My bill is 3× the estimate”
A 2026 pricing breakdown models the classic path: a team reads the rate card, projects ~$200/month, adds a few global secondary indexes, enables backups and a global table, and lands at $800–$2,400. The mechanism is unglamorous — each GSI is another synchronous write per base-table write, backups and streams bill separately, and global tables multiply writes per region. None of it is hidden; all of it is easy to leave out of the model.
Cosmos DB's DiskANN result
Microsoft's published research on integrating DiskANN into Cosmos DB NoSQL reports sub-20 ms query latency over a 10-million-vector index, recall that stays stable under updates, and roughly 43× and 12× lower query cost than two specialised serverless vector products. Take the comparison with the usual grain of salt — it's the vendor's own paper — but the architectural claim is the important one: for many RAG workloads a vector index inside the operational database beats a separate vector service on both operational complexity and cost. Your RAG track should test that claim rather than assume a dedicated vector DB.
Strong consistency across regions, without Spanner
DynamoDB global tables reached GA with multi-region strong consistency in June 2025 — RPO zero, latest-value reads from any replica region — including a witness region that participates in the consistency protocol without holding a full replica, which removes that region's storage, replicated-write and transfer charges. Design consequence: “strong consistency across regions requires relational distributed SQL” is no longer true. The NoSQL option is real, and the witness trick makes three-region quorums materially cheaper than they used to be.
Where each of these belongs in a GenAI serving stack
| Need | Shape | Concretely, on your stack |
|---|---|---|
| Prompt → response cache for repeated or near-identical requests | Cache | Redis/Valkey keyed on a hash of (model version + normalised prompt + sampling params). Include the model version in the key or a model swap serves stale generations. This is the highest-ROI cache in an inference stack — a cache hit costs a fraction of a cent and skips the GPU entirely. |
| Per-tenant rate limiting and quota | Cache / KV | Redis counters with TTL. Beware the hot-key pattern from above — a single high-volume tenant is exactly the shape that throttles. Shard the counter key. |
| Conversation / session history for multi-turn chat | Document | DynamoDB / Cosmos DB / Firestore, partition key = conversation_id, sort key = turn timestamp. Naturally high cardinality, naturally range-queried, exactly the shape these engines are best at. |
| Token-usage and latency telemetry | Wide-column or warehouse | Bigtable if you need per-second granularity at high write volume; otherwise straight to object storage and BigQuery/Redshift/Fabric. Row key tenant#reverse_ts, never ts#tenant. |
| Eval results, cost analysis, drift monitoring | Columnar OLAP | BigQuery over your logs in GCS. This is where “which model version had the best judge score per dollar” gets answered, and it's a scan-and-aggregate question by nature. |
| Model registry, deployments, job state | Relational | Session 3's answer, unchanged. Don't let the NoSQL enthusiasm of this tab move it. |
| Embeddings for retrieval | Vector (RAG track) | Flagged, not designed here. Note only that the 2026 default has shifted toward “vector index inside a database you already run” — pgvector on managed Postgres, Cosmos DB DiskANN, Spanner/BigQuery vector search — rather than a separate vector service. Test that in your RAG track. |
Lab S4 — build a hot partition on purpose, then fix it
Budget alert first. Smallest capacity settings only. Read the teardown before you start.
- Budget alert: $5, email at 50%.
- Create one NoSQL table in provisioned mode with the smallest capacity the service allows — on DynamoDB, 5 RCU / 5 WCU, which sits inside the always-free tier. Partition key
pk(string), sort keysk(string). - Write 200 items with
pk = "GLOBAL"andsk = <timestamp>as fast as a small script can. Watch the throttling metrics appear. This is the failure mode, reproduced in one minute for free. - Now write 200 items with
pk = "GLOBAL#" + random(0..9). Same total volume, no throttling. That's write sharding, and you've now felt the difference rather than read about it. - Turn on Contributor Insights (or the equivalent key-diagnostics feature) and confirm it names your hot key. Turn it back off afterwards — it bills separately.
- Query with a strongly-consistent read and then an eventually-consistent one, and compare the consumed capacity reported in the response. The 2× difference from Pass 3 becomes a number you've seen.
- Optional, on Cosmos DB instead: create a serverless account, set consistency to Session, then to Strong, and compare the RU charge for the same query. Same lesson, different currency.
- Delete the table. Provisioned throughput bills per hour, whether or not a single request arrives. A forgotten table provisioned at a few hundred capacity units is a genuinely unpleasant surprise, and unlike a VM there's nothing to “stop”.
- Delete global table replicas first if you created any — each replica region bills its own storage and replicated writes.
- Turn off Contributor Insights / diagnostics — separate line item, easy to forget.
- Delete on-demand backups and turn off point-in-time recovery. Backups outlive the table by design and keep billing for stored bytes.
- Delete any cache cluster immediately. An in-memory cache is billed on provisioned node-hours and is one of the more expensive things per hour in this stage — a small Redis node left running for a month costs far more than everything else in these four labs combined.
- If you ran a warehouse query, check the scanned-bytes charge and delete any datasets/tables you created. On per-TB-scanned billing, a single careless
SELECT *over a large public dataset can cost real money — always preview with a dry run and aLIMITwon't save you (it still scans). - Keep the budget alert. You now have four labs' worth of reasons why.
You should now be able to, without looking anything up
- Say why a database's data files go on block storage and its backups go on object storage.
- Convert 99.95% into minutes per month, and say what topology buys it.
- Explain why a strongly-consistent global read costs latency, and name two services that make you pay for it explicitly.
- Recite the four object-storage temperatures and the minimum-duration trap on each cloud — including that Google's Archive is 365 days.
- Diagnose “my NoSQL table throttles while showing spare capacity” in one sentence.
The five habits worth keeping
- Name the access shape before naming a service.
- Check the minimum duration before every tiering decision.
- Default to the boring managed relational database, and earn your way off it with measurements.
- Design the partition key from the access pattern, on paper, before you create the table.
- Write the expiration rule and the teardown step at the same time you create the resource.
State now has a correct home on all three clouds. The open question this stage deliberately left unanswered: when the region holding it disappears, how do you get it back — and how fast? That's resilience, backup and disaster recovery, where the RPO and RTO numbers you've been quoting all stage finally get designed against rather than described.