S3 object storage: classes, lifecycle, presigned URLs, durability
S3 is a flat object store: buckets, keys, storage classes with retrieval/min-duration tradeoffs, lifecycle policies, presigned URLs, 11-nines durability (not availability), and strong consistency. Pick the class by access pattern, not by hope.
A data team moves five years of raw event logs — 400 TB, read maybe twice a year for compliance — into S3 and leaves them in Standard, because “it’s just S3.” The monthly bill for that bucket alone reads like a second headcount. So someone scripts a lifecycle rule to dump everything older than 90 days into Glacier Flexible Retrieval and feels clever. Six weeks later an auditor needs a slice of last year’s data now, the team fires a bulk restore across millions of objects, and gets blindsided twice: the restore takes hours, and the per-GB retrieval fee on hundreds of terabytes lands a bill spike nobody had modeled. Neither the original choice nor the fix was wrong because of S3. They were wrong because nobody read the access pattern and matched it to a storage class — the single decision S3 forces on you and quietly bills you for every month you get it wrong.
Buckets, keys, and the flat keyspace
S3 is an object store, not a filesystem. An object is bytes plus metadata plus a key, and it lives in a bucket — a globally-named, region-scoped container. There are no real directories. The key logs/2026/06/04/app.log looks like a path, but it is a single flat string; the slashes are just characters. The console renders a folder tree as a convenience by splitting on /, but under the hood there is one giant lexicographic keyspace and nothing else. This matters because almost everything that scales in S3 scales by prefix — the leading portion of the key — not by folder, because folders don’t exist.
That flat model also shapes performance. S3 gives you at least 3,500 PUT/COPY/POST/DELETE and 5,500 GET/HEAD requests per second per prefix, and there is no cap on the number of prefixes. So request throughput is something you design by spreading keys across prefixes, not a fixed quota you hit. A bucket that funnels every write through one hot prefix (say, keys that all start with the same date) will throttle with 503 SlowDown long before the bucket itself is “full” — because a bucket has no size limit, only per-prefix request limits.
# A key is one flat string; the "folders" are just slashes in it.
aws s3api put-object \
--bucket app-prod-events \
--key "logs/2026/06/04/ingest-7f3a.json.gz" \
--body ingest-7f3a.json.gz \
--storage-class STANDARD
# List by prefix — this is what "ls a folder" actually does under the hood.
aws s3api list-objects-v2 \
--bucket app-prod-events \
--prefix "logs/2026/06/04/" \
--query "Contents[].{Key:Key,Class:StorageClass,Bytes:Size}"Since December 2020, S3 also gives strong read-after-write consistency for free, on every request, in every region. After a successful PUT — whether it’s a brand-new object, an overwrite, or a delete — any later GET, HEAD, or LIST immediately sees the latest state. The old eventual-consistency caveats (“you might read a stale or missing object right after writing”) are gone. You no longer need read-your-write workarounds; if a read looks stale today, the bug is in your code or caching layer, not in S3.
Storage classes: the retrieval-versus-min-duration tradeoff
When you store data in S3, the default class is never neutral — it is a billing decision you make once and then pay for silently every month. Before you reach for the first class on the list, ask yourself: how often will this data actually be read, and how fast does it need to come back? The answer drives every tradeoff below.
Every object carries a storage class, and that single attribute is the main cost lever. The classes trade three things against each other: per-GB storage price, retrieval latency, and a minimum storage duration you pay for even if you delete early. Numbers below are from the AWS docs; dollar figures are illustrative and region-dependent — always price against the S3 pricing page.
S3 Standard is the default: stored across ≥3 Availability Zones, millisecond access, no minimum duration, no retrieval fee. It is the right home for hot data and the wrong home for anything cold — you’re paying premium storage for bytes nobody reads.
Standard-IA (Infrequent Access) keeps the same ≥3-AZ resilience and millisecond access but cuts the storage price, in exchange for a per-GB retrieval fee and a 30-day minimum duration. One Zone-IA is cheaper still because it stores in only one AZ — same durability design, but if that AZ is physically lost, the data is gone. Intelligent-Tiering sidesteps the guessing: for a small per-object monitoring fee it moves each object between access tiers automatically as access patterns shift, with no retrieval fee and no minimum duration — the safe default when you genuinely don’t know the pattern.
The Glacier family is for archives. Glacier Instant Retrieval keeps millisecond access (90-day min duration) for rarely-read data you still need fast. Glacier Flexible Retrieval (90-day min) and Glacier Deep Archive (180-day min) are true cold storage: objects are archived and not available for real-time access — you must issue a RestoreObject and wait minutes to hours (Flexible) or hours (Deep Archive) before you can read them, and you pay a per-GB retrieval fee on top. That retrieval fee and latency are exactly what the Hook’s team walked into.
| Class | AZs | Retrieval latency | Min duration | Retrieval fee |
|---|---|---|---|---|
| Standard | ≥3 | Milliseconds | None | None |
| Standard-IA | ≥3 | Milliseconds | 30 days | Per-GB |
| One Zone-IA | 1 | Milliseconds | 30 days | Per-GB |
| Intelligent-Tiering | ≥3 | Milliseconds (active tiers) | None | None (monitoring fee) |
| Glacier Instant Retrieval | ≥3 | Milliseconds | 90 days | Per-GB |
| Glacier Flexible Retrieval | ≥3 | Minutes–hours (restore) | 90 days | Per-GB |
| Glacier Deep Archive | ≥3 | Hours (restore) | 180 days | Per-GB |
▸Why this works
Durability and availability are different numbers, and teams conflate them. Durability — designed for eleven nines (99.999999999%) across every class, even One Zone-IA — is the probability your bytes survive: lose-an-object odds so small they’re statistically near-zero. Availability is the probability you can reach the object right now, and it varies by class (S3 Standard targets 99.99%, Standard-IA 99.9%, One Zone-IA 99.5%). So One Zone-IA is as durable as Standard-IA on paper — until the one AZ holding it is physically destroyed, at which point durability stops mattering and you’ve simply lost the data. Eleven nines protects against disk and device failure across AZs; it does not protect One Zone-IA against losing its single AZ, and it never protects you against your own DeleteObject, a bad lifecycle rule, or ransomware. Durability is not backup.
Lifecycle policies and presigned URLs
You rarely move objects between classes by hand. A lifecycle configuration on the bucket does it on a schedule: transition objects to a colder class after N days, and expire (delete) them after M days. This is how you encode “hot for a month, infrequent for a quarter, archived for a year, gone after seven years” as one rule instead of a cron job.
{
"Rules": [
{
"ID": "events-cooling",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER" },
{ "Days": 365, "StorageClass": "DEEP_ARCHIVE" }
],
"Expiration": { "Days": 2555 }
}
]
}The second workhorse is the presigned URL. By default every object is private — only the owner can read it. A presigned URL embeds a time-limited, cryptographic signature (SigV4) so anyone holding the URL can perform one specific operation — a GET to download or a PUT to upload — until it expires. The signature carries the permissions of whoever generated it, so it can never grant more access than that signer already has. The standard pattern: your server signs a PUT URL and hands it to the browser, which uploads the file straight to S3 — your application servers never touch the bytes, so a 2 GB upload doesn’t tie up an app process or count against your egress. The expiry is the whole security model: an SDK/CLI presigned URL can live up to 7 days (604,800 seconds); the console caps at 12 hours. Sign short.
import boto3
s3 = boto3.client("s3")
# A time-limited PUT URL: hand this to the browser so it uploads
# directly to S3. Expires in 15 minutes; carries the signer's perms.
upload_url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={"Bucket": "app-prod-uploads", "Key": "user/42/avatar.png"},
ExpiresIn=900, # seconds; max 604800 (7 days) for SigV4
)Compliance logs: ~50 TB/year, write-once, almost never read, but when an auditor asks you must produce a slice within a day or two (a few hours of wait is fine). Retention 7 years. Pick the storage class for the aged data.
Your bucket throttles with 503 SlowDown under heavy write load, even though the bucket is nowhere near 'full'. What's the most likely cause?
A teammate says 'S3 is eleven nines durable, so we don't need backups.' Why is that wrong?
- 01Walk through the S3 storage classes and the three things they trade off, then give the rule for picking one.
- 02Distinguish durability from availability in S3, and explain why eleven nines is not a backup.
S3 is an object store with a single flat keyspace — keys only look like paths, and everything that scales does so per prefix, giving roughly 3,500 writes and 5,500 reads per second per prefix with no bucket size limit and free strong read-after-write consistency since December 2020. The decision S3 forces on you is the storage class, the main cost lever: Standard for hot data (≥3 AZs, millisecond, no minimum, no retrieval fee); Standard-IA and One Zone-IA for infrequent access at lower storage price but with a per-GB retrieval fee and a 30-day minimum, where One Zone-IA’s single AZ means physical AZ loss destroys the data; Intelligent-Tiering to auto-move objects with no retrieval fee or minimum when the pattern is unknown; and the Glacier family for archives — Instant Retrieval keeps millisecond access (90-day min), while Flexible Retrieval (90-day min, restore in minutes-to-hours) and Deep Archive (180-day min, restore in hours) are true cold storage with per-GB retrieval fees that ambush teams who archive data they later need fast. Lifecycle configurations transition and expire objects on a schedule so you encode cooling as one rule; presigned URLs hand a browser a time-limited signed GET or PUT (up to 7 days) so uploads and downloads bypass your servers entirely. And durability is not availability and not backup: eleven nines means your bytes survive hardware failure, but it never protects against your own deletes, a bad lifecycle rule, ransomware, or — for One Zone-IA — losing the one AZ that holds the only copy. The whole skill is reading the access pattern first and letting it pick the class, because S3 bills you every month you guess wrong. Now when you see a runaway S3 bill or a surprise retrieval spike, your first question is: which storage class is this data sitting in, and does the access pattern actually match it?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.