AWS Gotchas: S3 Naming, Lambda Limits, and Region Surprises
Why your S3 bucket name has to be globally unique, what Lambda's 15-minute timeout actually means, the IAM role vs user distinction, and the free tier traps that show up on your bill.

AWS has more than 200 services and each has its own quirks. Most production AWS bugs aren't exotic — they're someone hitting a hard limit they didn't know existed, or trusting a default that does something unexpected. Here are the ones worth memorizing.
1. S3 bucket names are globally unique across all AWS accounts
``bash
aws s3 mb s3://my-data
# Error: BucketAlreadyExists
``
Bucket names live in a single global namespace. my-data, backup, static-assets — all taken. You'll need a unique prefix (often your company name or a UUID).
- Bucket names also have to be:
- 3-63 chars
- Lowercase letters, numbers, hyphens, periods only
- No consecutive periods
- Can't look like an IP address
2. S3 read-after-write is now strongly consistent (since Dec 2020)
This used to be a major gotcha — uploading an object and immediately reading it might return the old version. As of December 2020, S3 has strong read-after-write consistency. The gotcha is now mostly historical, but you'll see workarounds in older code.
If you maintain code that does waitForObject() retries after uploads, you can probably delete those.
3. Lambda's 15-minute timeout is a hard ceiling
``python
def handler(event, context):
# Even if you set timeout = 900 seconds (15 min),
# AWS kills the function at 15 min, no exceptions.
``
- For longer jobs:
- Use Step Functions (orchestrate multiple Lambdas)
- Switch to ECS/Fargate
- Use Batch for compute-heavy jobs
Common bug: a Lambda that processes a large file works in testing but times out on real data because the file is bigger than expected.
4. Lambda cold starts vs warm invocations
A "cold start" happens when AWS spins up a new Lambda container — usually 100ms-1s extra latency (much more for VPC Lambdas). Frequent invocations stay warm; sporadic ones don't.
The classic bug: dev tests with rapid-fire invocations show 50ms response times. Prod traffic at 1 req/min shows 1s+ response times because every invocation is cold.
Workarounds: Provisioned Concurrency (paid), pinging the function every minute (free but feels gross), or accepting cold starts.
5. Region vs Availability Zone vs Edge Location
Hierarchy:
- Region: geographic area (e.g.
us-east-1= Northern Virginia) - Availability Zone (AZ): isolated data center within a region (
us-east-1a,us-east-1b, ...) - Edge Location: CloudFront cache POP, geographically distributed
- Common bugs:
- Resources in different regions can't directly talk via VPC peering without setup
- S3 buckets are regional but their names are global
- IAM is global (one set of users/roles for all regions)
- Resources in
us-east-1aon account A andus-east-1aon account B are NOT the same physical AZ — AWS randomizes AZ-to-letter mapping per account
6. IAM Users vs Roles vs Policies
- User: a person or service with permanent credentials (access key + secret)
- Group: a collection of users for bulk policy assignment
- Role: assumed temporarily, no permanent credentials. Used for cross-account access and for services-acting-on-behalf-of (e.g., a Lambda assuming a role)
- Policy: a JSON document granting/denying actions on resources
The most common mistake: hardcoding access keys for an IAM User into EC2 or Lambda. Use a Role instead — credentials rotate automatically and you don't ship them in source code.
7. IAM is eventually consistent — wait for changes to propagate
``bash
aws iam attach-user-policy --user-name foo --policy-arn ...
aws s3 ls # might still fail for a few seconds
``
When you create/modify IAM resources or policies, they can take 10-30 seconds to propagate globally. Tests that immediately use new credentials are flaky for this reason.
8. EC2 instance store is ephemeral — EBS is persistent
- ```
- Instance store: lost on stop/terminate, included with some instance types
- EBS: persistent block storage, snapshottable, separately billed
- ```
The classic bug: dev tests on an EC2 with instance store, "stops" the instance to save money, comes back and the data is gone. Always use EBS for anything you care about.
EBS volumes are also tied to a specific AZ — you can't attach an EBS volume in us-east-1a to an EC2 in us-east-1b. Take a snapshot, restore in the other AZ.
9. CloudFormation drift detection doesn't catch everything
If you edit a resource through the console after deploying it via CloudFormation, the template no longer reflects reality. Drift detection helps but doesn't catch:
- Tags added/removed
- Some property updates
- Nested resources
The fix: discipline. Everything goes through IaC (CloudFormation/CDK/Terraform), nothing edited in console.
10. Free tier traps that show up on your bill
The Free Tier is generous but has traps:
- NAT Gateway: NOT free, ~$32/mo + data charges. If your VPC has private subnets, this can blindside you.
- Elastic IPs not attached: charged $0.005/hour (~$3.60/mo) if unattached
- Load balancers: ~$16/mo for ALB even with no traffic
- CloudWatch Logs: free for 5GB/month ingest, 5GB/month archived. After that, $0.50/GB ingest, $0.03/GB stored
- S3 PUT requests: free tier covers 2000/month. Easy to exceed with logging.
- Cross-region/cross-AZ data transfer: $0.01-0.02/GB. Multiple TB will add up.
If you stop or terminate an instance but leave the EBS volume attached, the volume keeps billing.
11. s3:ListBucket vs s3:GetObject are separate permissions
``json
{
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
``
This lets you GET objects but NOT list the bucket. A user with only GetObject can't browse — they have to know the exact key.
To list contents, you also need:
``json
{
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket" // note: no /*
}
``
Note the resource arn format differs: /* for object operations, no suffix for bucket-level operations.
12. s3:GetObject doesn't include s3:GetObjectVersion
If your bucket has versioning enabled, getting an old version requires GetObjectVersion. This often surprises devs who turn on versioning and find their existing IAM policies still work for the latest version but fail on prior versions.
13. CloudFront cache invalidations cost money after the first 1000/month
``bash
aws cloudfront create-invalidation --distribution-id ... --paths "/*"
``
You get 1000 invalidation paths free per month. After that, $0.005 per path. "/*" counts as 1 path. "/css/style.css", "/js/app.js" counts as 2.
Devs who invalidate every file on every deploy can rack up surprising bills.
Need help with an AWS bill or architecture review?
We've audited dozens of AWS bills and found surprises every time. NAT Gateways nobody knew about, EBS volumes from terminated instances still billing, S3 buckets with versioning chewing through storage. If your AWS bill is going up faster than your traffic, we can fix it.
Need Help With Your Website?
I fix these problems every day. Send me a message and I'll take a look.
Get Help Now