Amazon S3 (Simple Storage Service)
Object storage. The oldest AWS service (2006) and the most foundational — most other services touch S3. Eleven-nines durability, virtually unlimited scale, accessible from anywhere by URL.
The model
- Bucket — a named container, globally unique across AWS.
- Object — a file + metadata. Key is the “path”, but S3 is a flat namespace (no real directories).
- Region — buckets are regional; replicate across regions explicitly.
import boto3
s3 = boto3.client("s3")
s3.put_object(Bucket="my-app-uploads", Key="users/42/avatar.png", Body=image_bytes,
ContentType="image/png")
obj = s3.get_object(Bucket="my-app-uploads", Key="users/42/avatar.png")
data = obj["Body"].read()
Storage classes
| Class | Use case | Cost (relative) |
|---|---|---|
| Standard | hot data, frequent access | baseline |
| Intelligent-Tiering | unknown / mixed access patterns | small mgmt fee, auto-tier |
| Standard-IA (Infrequent Access) | warm data, occasional access | ~40% cheaper, retrieval fee |
| One-Zone-IA | reproducible warm data | ~50% cheaper, single AZ |
| Glacier Instant | cold data, occasional retrieval | ~60% cheaper |
| Glacier Flexible | archives, restore in minutes-hours | ~80% cheaper |
| Glacier Deep Archive | rarely-touched archives | cheapest, 12h+ restore |
For most apps: Intelligent-Tiering on user uploads + Standard on hot operational data. Lifecycle rules transition older objects to Glacier.
Lifecycle policies
{
"Rules": [{
"ID": "transition-and-expire",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
],
"Expiration": {"Days": 2555}
}]
}
After 30 days, transition to IA; after 90 to Glacier; expire (delete) at 7 years. Saves an enormous amount of money on log data.
Versioning
Per-bucket toggle. After enable, overwrites and deletes preserve old versions; you can list and restore.
s3 = boto3.client("s3")
versions = s3.list_object_versions(Bucket="orders-archive", Prefix="reports/2024-01")
Delete protection: a “delete” actually creates a delete marker; the object still exists at the previous version. MFA Delete requires MFA token to delete versions, for paranoid setups.
Side effect: storage grows. Pair with lifecycle rules to expire old versions:
{"NoncurrentVersionExpiration": {"NoncurrentDays": 30}}
Presigned URLs
Generate a URL with embedded auth + expiration; share with anyone:
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "uploads", "Key": "report.pdf"},
ExpiresIn=3600, # 1 hour
)
The dominant pattern for direct browser uploads / downloads without proxying through your app:
# Direct upload: backend generates presigned PUT URL, client uploads directly
url = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "uploads", "Key": f"users/{user_id}/avatar.png", "ContentType": "image/png"},
ExpiresIn=900,
)
return {"upload_url": url}
The client PUTs directly to S3, your backend never sees the bytes. Huge for media-heavy apps.
For form-based uploads (with field restrictions): generate_presigned_post.
Encryption
- SSE-S3 — AWS-managed keys; free; default on new buckets.
- SSE-KMS — AWS KMS keys; per-API-call KMS cost; auditable in CloudTrail.
- SSE-C — customer-provided keys (rare).
- Client-side — encrypt before upload.
For most regulated workloads: SSE-KMS with a customer-managed CMK. The CMK key policy controls who can decrypt — separates “can access S3” from “can read the data”.
Access control
Three layers, evaluated together:
- Bucket policy — JSON IAM-style policy on the bucket. Allow/deny by principal, action, resource, condition.
- IAM policies — on users/roles. Standard IAM.
- ACLs (legacy) — disabled on new buckets; don’t use.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowAccountReadOnly",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/app"},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
}]
}
Block Public Access is the safety net. Turn on at the bucket and account level unless you’re consciously running a public bucket (static site hosting).
S3 + CloudFront for static sites
Bucket holds the static files; CloudFront fronts them with TLS, caching, custom domain, WAF. Origin Access Control (OAC) restricts the bucket to only CloudFront — no direct internet access.
client → CloudFront → S3 (private, OAC)
Cheap, fast, scalable. The pattern behind 99% of “static SPAs on AWS”.
S3 Events
S3 emits events on object creation / deletion. Targets:
- Lambda — process new uploads.
- SQS — buffer for batch processing.
- SNS — fan-out.
- EventBridge — richer event routing.
# Lambda triggered on s3:ObjectCreated:*
def handler(event, context):
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
process(bucket, key)
The “image uploaded → resize” pipeline is the canonical example.
Object Lambda
Transform on the fly between S3 and the client — masking PII, resizing images, redacting columns from CSVs — without storing the variant.
# Object Lambda function
def handler(event, context):
s3 = boto3.client("s3")
original = requests.get(event["getObjectContext"]["inputS3Url"]).content
transformed = mask_pii(original)
s3.write_get_object_response(
RequestRoute=event["getObjectContext"]["outputRoute"],
RequestToken=event["getObjectContext"]["outputToken"],
Body=transformed,
)
Multipart upload
For objects > 5GB (mandatory) or large objects on flaky networks (recommended). Boto3 handles it transparently via upload_file:
s3.upload_file("big_video.mp4", "uploads", "videos/abc.mp4")
# Internally: multipart if > threshold
Failed multipart uploads leave orphan parts costing storage. Lifecycle rule to clean:
{"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}}
Strong consistency
S3 has read-after-write consistency for all operations since 2020. PUT then GET sees the new object. No more eventual-consistency surprises.
But: list-after-write consistency is also strong now. ListObjects immediately after a PutObject includes the new key.
Common gotchas
ListObjectspaginates at 1000. Uselist_objects_v2withContinuationTokenorpaginator.- Path-style vs virtual-hosted-style URLs. Path-style is being deprecated; use
bucket.s3.region.amazonaws.comURLs. - Default
bucket-owner-full-control. Cross-account writers need to set ACL or you can’t read their uploads. - CORS for browser uploads. Set CORS on the bucket; otherwise direct uploads via presigned URLs blow up.
- Naming a bucket the same as another in the world. Bucket names are globally unique;
my-appis taken since 2007. - HEAD before GET to check existence.
head_objectraises 404 on missing; cleaner thanget_object+ catch. - Big bills from
S3 Standardon cold data. Always set lifecycle rules; without them, logs from 5 years ago still cost full price.
S3 Select / Athena
Query S3 data without downloading:
- S3 Select — SQL on a single object (CSV, JSON, Parquet). Cheap if you grep one file.
- Athena — serverless SQL across many objects, partitioned. Pay per TB scanned.
For analytics on S3 data lakes: Athena (or Redshift Spectrum, or Trino).
Common interview pattern: “upload + process”
1. Backend generates presigned PUT URL → client.
2. Client uploads directly to S3.
3. S3 emits ObjectCreated event → SQS.
4. Lambda / Fargate worker reads SQS, processes the object.
5. Worker writes result back to S3 / DB.
6. (Optional) Notify user via WebSocket / push.
Critical: idempotent processing, since SQS delivery is at-least-once.
Interview angle
- “How do you handle large file uploads from a browser without proxying through your backend?” — backend generates a presigned PUT URL (with
Content-Type,Content-Lengthconstraints, short expiry); client uploads directly to S3. Orgenerate_presigned_postfor form-based uploads. Set bucket CORS. - “What storage class for log files?” — Standard for the first 30 days (probably grepped during incidents), transition to Standard-IA at 30d, Glacier at 90d, expire at retention horizon. Lifecycle policy does this automatically.
- “How do you secure a private S3 bucket fronted by CloudFront?” — Origin Access Control (OAC). Bucket policy allows the CloudFront distribution; bucket has Block Public Access on. Clients can only reach the content via CloudFront (TLS + caching + WAF).
- “What’s the consistency model?” — strong read-after-write since Dec 2020. PUT then immediate GET sees the new object. List operations are also strongly consistent now.
- “How do you trigger work on a new upload?” — S3 event notifications → SQS/Lambda/SNS/EventBridge. Pattern: S3 → SQS → worker (idempotent). Lambda direct works for low-volume; SQS gives buffering and retry.
- “How do you handle PII masking in objects?” — Object Lambda. Transforms responses on the GET path without storing variants. Or, for batch: separate “masked” bucket populated by a pipeline.