TL;DR
- S3 is object storage, not a filesystem - a flat namespace of
key → object, not folders. - 11 nines of durability (99.999999999%) comes from replication across multiple Availability Zones.
- Storage classes trade retrieval speed/cost for storage cost - match them to access patterns.
- Presigned URLs let clients upload/download directly without touching my server.
- The #1 risk is accidental public access - Block Public Access + least-privilege policies.
Why S3 first
I'm officially on my AWS journey now, and I started with S3 on purpose. In the scaling post I learned that stateless servers must push files off local disk. S3 is the place that state goes. Nearly every AWS architecture touches it, so it's the right foundation before compute.
Object storage isn't a filesystem
This was my first reframe. S3 has no real directories. It's a flat map of keys to objects within a bucket:
bucket: taskflow-uploads
key: avatars/user-42/profile.png → object (bytes + metadata)
key: exports/2026-05/report.csv → object
Those slashes are just part of the key string. The console shows avatars/ as a folder, but it's an illusion built from key prefixes. Understanding this matters because S3 list operations and lifecycle rules work on prefixes, not directories - and listing by prefix is how you efficiently query a "folder."
Each object also carries metadata (content type, cache headers, custom tags) and is identified globally by bucket + key.
Durability and availability
S3 Standard stores every object redundantly across at least 3 Availability Zones (physically separate data centers in a region). That's where "11 nines" of durability comes from - statistically, you'd lose one object every ~10 million years. Durability (won't lose it) and availability (can reach it right now) are different guarantees, and S3 quotes both.
One subtlety I made sure to learn: S3 now provides strong read-after-write consistency. Write an object, immediately read it, and you get the latest version - no more "eventual consistency" surprises that older AWS tutorials warn about.
Storage classes - paying for the right thing
Not all data is accessed equally, and S3 prices that in:
- S3 Standard - hot data, frequent access. Default.
- Intelligent-Tiering - S3 watches access patterns and moves objects between tiers automatically. My default when I don't know the pattern.
- Standard-IA / One Zone-IA - infrequent access; cheaper storage, you pay a retrieval fee.
- Glacier Instant / Flexible / Deep Archive - archival. Deep Archive is dirt cheap to store but retrieval takes hours.
I set a lifecycle policy so user exports transition Standard → Standard-IA after 30 days and to Glacier after 90 - automatic cost savings with zero code.
Presigned URLs - keep big files off my server
Routing a 50MB upload through my API wastes bandwidth and ties up a request. The better pattern is a presigned URL: my server generates a short-lived, signed URL granting permission for one specific operation, and the client talks to S3 directly.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const url = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: "taskflow-uploads", Key: `avatars/${userId}.png` }),
{ expiresIn: 300 } // valid for 5 minutes
);
// client PUTs the file straight to S3 with this URLMy server never handles the bytes - it just hands out time-boxed permission. This is how avatar and attachment uploads should work at scale.
Security - where people get burned
Public S3 buckets are a recurring cause of real-world data breaches, so I was deliberate here:
- Block Public Access is on at the account and bucket level. On by default now, and I leave it that way unless I'm intentionally hosting public static assets.
- IAM policies vs. bucket policies: IAM policies attach to users/roles ("who can do what"); bucket policies attach to the bucket ("who can touch this resource"). I grant my app's IAM role least-privilege access - only
s3:GetObject/s3:PutObjecton the specific prefix it needs, nothing more. - Encryption at rest: SSE-S3 (S3-managed keys) by default; SSE-KMS when I need audit trails and key rotation control.
- Versioning on important buckets so an overwrite or delete is recoverable.
Bonus: S3 as an event source
S3 can emit event notifications (s3:ObjectCreated:*) to Lambda, SQS, or SNS. So "user uploaded an image → generate a thumbnail" becomes event-driven, echoing the Kafka decoupling lesson - the upload triggers downstream work without my API orchestrating it.
Takeaways
- It's a key-value object store, not a filesystem. Prefixes, not folders.
- Match the storage class to the access pattern and automate it with lifecycle rules.
- Presigned URLs keep large transfers off your servers.
- Assume buckets are private; prove you meant to make one public.
Next
I can store data in the cloud now, but my app still runs on a server I babysit. Next in my AWS journey: actually running my containers in the cloud with ECS and Fargate.