Study interactive :: Progress tools open in the Study Hub reader.

18. Object Storage

A relational DB is great for structured rows. It's terrible at storing a 4 GB video. Object storage is what you reach for instead.

If you've ever used Google Drive or Dropbox, you've used object storage. If you've ever uploaded a profile photo to a website, it probably ended up in S3.

What it actually is

Object storage is a flat key-value store where:

+----------------------+----------------------+--------+
|         Key          |        Value         | Size   |
+----------------------+----------------------+--------+
| users/42/avatar.jpg  | (jpeg bytes)         | 87 KB  |
| videos/abc/raw.mp4   | (mp4 bytes)          | 1.2 GB |
| backups/2026-05-26.tar | (tar bytes)        | 4.7 GB |
+----------------------+----------------------+--------+

That's it. No tables. No queries. No indexes (except by key prefix). Just "put this blob at this key", "get this blob by key", "list keys starting with this prefix", "delete this key".

The big providers:

Provider Service
AWS S3
Google Cloud Cloud Storage (GCS)
Azure Blob Storage
Cloudflare R2
Self-hosted MinIO, Ceph

They all speak roughly the same model and similar HTTP APIs.

What it's good at

What it's bad at

The API in three calls

S3 SDK, Python:

import boto3

s3 = boto3.client("s3", region_name="us-east-1")

# Upload
s3.put_object(
    Bucket="my-app-uploads",
    Key="users/42/avatar.jpg",
    Body=open("avatar.jpg", "rb"),
    ContentType="image/jpeg",
)

# Download
obj = s3.get_object(Bucket="my-app-uploads", Key="users/42/avatar.jpg")
data = obj["Body"].read()

# List
result = s3.list_objects_v2(Bucket="my-app-uploads", Prefix="users/42/")
for item in result["Contents"]:
    print(item["Key"], item["Size"])

That's almost the whole API. There's also delete_object, copy_object, and some metadata calls.

Pre-signed URLs

A common pattern: you want users to upload directly to S3 without proxying through your server.

The server creates a "pre-signed URL" that authorizes the user to PUT to a specific key, for a short time:

url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "my-app-uploads", "Key": "users/42/avatar.jpg"},
    ExpiresIn=300,   # 5 minutes
)
return {"upload_url": url}

Frontend then does:

await fetch(uploadUrl, { method: "PUT", body: file });

Your server never touches the bytes. Great for huge files, terrible for your bandwidth bill if you didn't think of this.

Storage classes

Real-world data isn't all equal. Some files are hit constantly. Some haven't been read in a year. S3 lets you put them in different tiers:

Tier Cost per GB/mo Retrieval cost Use case
S3 Standard ~$0.023 None Hot data
S3 Standard-IA ~$0.0125 Higher Backups read occasionally
S3 One Zone-IA ~$0.01 Higher Non-critical backups
S3 Glacier Instant ~$0.004 Higher Archive, sometimes needed
S3 Glacier Flexible ~$0.0036 Minutes to hours Long-term backups
S3 Glacier Deep Archive ~$0.00099 12+ hours "Set and forget"

You can move objects through tiers automatically with lifecycle rules. A typical setup: "move objects to IA after 30 days, Glacier after 90, delete after 7 years".

Object storage as the backbone of modern systems

Look behind the curtain of many products and you'll find object storage doing the heavy lifting:

When you see "petabyte data lake" or "object store", it's the same idea: flat namespace, blobs, cheap, durable.

A few practical patterns

Don't put PII in the key

The key users/[email protected]/avatar.jpg leaks email addresses. Use opaque IDs: users/8f3a-2b1c/avatar.jpg.

Use a CDN in front

S3 is global but not optimized for latency. Putting CloudFront (or Cloudflare's R2-with-cache) in front of your bucket is the easy speed-up.

Versioning and lifecycle

Turn on versioning if you're storing important data. It keeps old copies when you overwrite or delete. Combine with lifecycle to delete the old versions after N days, otherwise the bucket grows forever.

Encryption

Server-side encryption (SSE-S3 or SSE-KMS) costs nothing and avoids a class of compliance headaches. Turn it on by default.

Don't make buckets public by accident

Famously, lots of data breaches are "public S3 bucket". Bucket policies are restrictive by default in modern AWS, but it's still a thing to double check.

Where it sits in your architecture

mint pre-signed URLPUT bytesUserCDNObject storeApp APIClientPostgres metadata

The DB stores "user 42 has avatar at key X". The object store stores the bytes. The CDN serves them quickly.

Things to remember

Going deeper