Skip to main content

S3 Is Not Just Storage — 10 Things Most Engineers Get Wrong

· 5 min read
Goel Academy
DevOps & Cloud Learning Hub

Most engineers treat S3 like a hard drive in the cloud — upload files, download files, done. But S3 is actually a full-featured data platform, and misunderstanding it costs companies thousands of dollars every month. Here are 10 things you're probably getting wrong.

1. Bucket Policies vs ACLs — Pick One

AWS provides two ways to control access to S3: bucket policies and ACLs. The correct answer in 2025 is bucket policies only. ACLs are a legacy feature that AWS themselves recommend disabling.

# Block all public access and disable ACLs (do this for every bucket)
aws s3api put-public-access-block \
--bucket my-app-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

A clean bucket policy looks like this:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-app-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}

2. Versioning Saves You From Yourself

Without versioning, deleting a file is permanent. With versioning, every change creates a new version and "deletes" just add a delete marker.

# Enable versioning on a bucket
aws s3api put-bucket-versioning \
--bucket my-app-bucket \
--versioning-configuration Status=Enabled

# List all versions of a file
aws s3api list-object-versions \
--bucket my-app-bucket \
--prefix config/app-settings.json

# Restore a previous version by copying it
aws s3api copy-object \
--bucket my-app-bucket \
--copy-source my-app-bucket/config/app-settings.json?versionId=abc123 \
--key config/app-settings.json

But here's the trap: versioning keeps every old version forever unless you set lifecycle rules. That 2MB config file you update 50 times a day? That's 100MB/day of hidden storage costs.

3. Storage Classes — Stop Paying for Hot Storage

This is where most teams hemorrhage money. Not every file needs instant access.

Storage ClassUse CaseMin StorageRetrieval CostMonthly Cost (per GB)
S3 StandardFrequently accessed dataNoneNone$0.023
S3 Standard-IAInfrequent access (>30 days)30 daysPer-GB fee$0.0125
S3 One Zone-IAReproducible infrequent data30 daysPer-GB fee$0.01
S3 Glacier InstantArchive with ms retrieval90 daysPer-GB fee$0.004
S3 Glacier FlexibleArchive, minutes to hours90 daysPer-GB + request$0.0036
S3 Glacier Deep ArchiveCompliance, 12-hour retrieval180 daysPer-GB + request$0.00099

Prices are for us-east-1 as of 2025. Check the pricing page for current rates.

4. Lifecycle Rules Automate Cost Savings

Don't manually move objects between storage classes. Automate it:

{
"Rules": [
{
"ID": "ArchiveOldLogs",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
},
{
"Days": 365,
"StorageClass": "DEEP_ARCHIVE"
}
],
"NoncurrentVersionExpiration": {
"NoncurrentDays": 30
}
}
]
}
# Apply the lifecycle configuration
aws s3api put-bucket-lifecycle-configuration \
--bucket my-app-bucket \
--lifecycle-configuration file://lifecycle.json

That NoncurrentVersionExpiration is critical — it cleans up old versions so versioning doesn't explode your bill.

5. Encryption — There's No Reason Not To

Every S3 bucket should have encryption enabled. Since January 2023, AWS encrypts all new objects with SSE-S3 by default, but you should understand your options:

Encryption TypeKey ManagementCostUse Case
SSE-S3AWS manages keysFreeDefault, good enough for most
SSE-KMSAWS KMS manages keys~$1/key/month + API callsAudit trail, key rotation control
SSE-CYou provide keys per requestFree (you manage keys)Regulatory requirements
# Set default encryption to SSE-KMS
aws s3api put-bucket-encryption \
--bucket my-app-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/my-s3-key"
},
"BucketKeyEnabled": true
}]
}'

Enable BucketKeyEnabled to reduce KMS API costs by up to 99%.

6. Pre-Signed URLs for Temporary Access

Need to let a user download a private file without making the bucket public? Pre-signed URLs:

# Generate a pre-signed URL valid for 1 hour (3600 seconds)
aws s3 presign s3://my-app-bucket/reports/quarterly-2025.pdf \
--expires-in 3600

This returns a URL with embedded credentials that expires automatically. Perfect for download links in emails, temporary file sharing, and allowing uploads without AWS credentials.

7. Static Website Hosting — The $0.50/Month Website

S3 can serve a static website directly, but you should always pair it with CloudFront:

# Enable static website hosting
aws s3 website s3://my-website-bucket \
--index-document index.html \
--error-document error.html

# Sync your build folder
aws s3 sync ./dist s3://my-website-bucket --delete

8. Event Notifications — Trigger Actions on Upload

S3 can notify Lambda, SQS, or SNS when objects are created, deleted, or restored:

# Configure event notification to trigger Lambda on new uploads
aws s3api put-bucket-notification-configuration \
--bucket my-app-bucket \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:process-upload",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"FilterRules": [{"Name": "prefix", "Value": "uploads/"}]
}
}
}]
}'

9. Cross-Region Replication for Disaster Recovery

Replicate objects automatically to another region:

# Enable replication (requires versioning on both buckets)
aws s3api put-bucket-replication \
--bucket my-app-bucket \
--replication-configuration '{
"Role": "arn:aws:iam::123456789012:role/S3ReplicationRole",
"Rules": [{
"Status": "Enabled",
"Destination": {
"Bucket": "arn:aws:s3:::my-app-bucket-replica",
"StorageClass": "STANDARD_IA"
}
}]
}'

10. The Cost Traps Nobody Warns You About

  • Request costs: ListObjects calls cost $0.005 per 1,000 requests. An app checking "does this file exist?" in a loop can generate thousands of dollars in charges.
  • Data transfer out: Free into S3, but $0.09/GB out to the internet. Use CloudFront to reduce this.
  • Incomplete multipart uploads: Failed large uploads leave fragments that cost storage. Add a lifecycle rule to clean them up.
{
"Rules": [{
"ID": "CleanupIncompleteUploads",
"Status": "Enabled",
"Filter": {},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 7
}
}]
}

What's Next?

You've got IAM and S3 locked down. Next up: EC2 instances — we'll break down instance types, pricing models, and why you're probably paying 60% more than you need to for your compute.


This is Part 3 of our AWS series. S3 is deceptively deep — bookmark this one for reference.