AWS S3 URL Formats Explained — Path Style vs Virtual Hosted Style
The AWS S3 URL format path style vs virtual hosted debate has burned more hours of my debugging time than I care to admit. A few years back, I was migrating a legacy data pipeline to a new region, confident the whole thing would take an afternoon. Six hours later I was staring at a wall of 403 Forbidden errors, convinced I had an IAM policy problem. I didn’t. I had a URL format problem. This article is the reference I wish I’d had that day — structured so you can actually find what you need without wading through AWS documentation that reads like it was written for a compliance audit.
The Two URL Formats — Side by Side
There are exactly two URL formats S3 uses. They contain the same information. The difference is where the bucket name lives in the URL.
Path Style
The bucket name sits in the URL path, after the domain:
https://s3.{region}.amazonaws.com/{bucket}/{key}
Real example, using a bucket called my-data-exports in us-east-2, accessing a file at reports/2024/january.csv:
https://s3.us-east-2.amazonaws.com/my-data-exports/reports/2024/january.csv
You’ll also see the legacy format without a region subdomain — https://s3.amazonaws.com/my-data-exports/reports/2024/january.csv — in older code. That’s path style pointing at us-east-1 implicitly. Don’t use it for new work.
Virtual Hosted Style
The bucket name becomes part of the subdomain:
https://{bucket}.s3.{region}.amazonaws.com/{key}
Same file, same bucket, virtual hosted format:
https://my-data-exports.s3.us-east-2.amazonaws.com/reports/2024/january.csv
That’s the one AWS wants you using. The key (the object path) is identical between both formats. The bucket moves. That’s the whole difference structurally, but the operational implications are significant enough that it’s worth understanding why.
The Region Note
Both formats should include the region. Omitting it technically works in some cases but it creates redirect latency and will eventually break on buckets created after September 2020 in non-us-east-1 regions. Put the region in. Always.
Why AWS Is Deprecating Path Style
AWS announced the deprecation of path-style access in 2019, targeting September 2020 as the cutoff for new buckets. Buckets created before that date kept path-style working. Buckets created after September 30, 2020 do not support path-style URLs by default.
The technical reasons are worth knowing, not just the timeline.
DNS Routing and Scale
With path style, every S3 request resolves through a small number of AWS-controlled domains like s3.us-east-1.amazonaws.com. AWS can’t do per-bucket DNS optimization. With virtual hosted style, each bucket gets its own DNS entry — my-bucket.s3.us-east-1.amazonaws.com — which lets AWS route traffic more granularly, apply DDoS mitigation per bucket, and scale independently per customer namespace.
TLS Certificate Implications
Path style puts all buckets under a shared AWS domain. Wildcard certificates handle this fine but it limits options for custom SSL behavior. Virtual hosted style is what makes features like S3’s custom domain support with CloudFront actually work cleanly at the DNS layer.
What This Means for Your Code
If you created buckets before October 2020 — path style still works on those. AWS has been cautious about pulling the rug out. But any bucket you’ve created since then, and every bucket you’ll ever create going forward, requires virtual hosted style. The practical answer is to default to virtual hosted everywhere and treat path style as a specific exception you reach for deliberately.
Burned by this exact assumption in production once. We had a Terraform module that had been working fine for two years, someone spun it up in a new account, new region, post-deprecation bucket, and the application just started throwing access errors on day one. The IAM policies were fine. The bucket policy was fine. The URL format in our config file was two years old.
When You Still Need Path Style
Path style isn’t dead. There are three real scenarios where you’ll reach for it.
Legacy Applications You Can’t Refactor
If you have an application built before 2019 that hardcodes S3 endpoints or uses an SDK version old enough that virtual hosted isn’t the default, and refactoring isn’t on the table, you need path style. This is more common in enterprise environments than people admit publicly. The answer is to make sure those applications only interact with pre-October-2020 buckets and add that constraint to your runbook explicitly.
S3-Compatible Storage — MinIO, Ceph, Wasabi
This is probably the most common reason developers hit path style in 2024. MinIO, Ceph, Wasabi, DigitalOcean Spaces, Backblaze B2 — these S3-compatible storage systems often default to or require path style. MinIO in particular, when self-hosted, typically uses path style unless you’ve configured DNS to support virtual hosted subdomains.
Frustrated by a MinIO setup refusing connections in a local Docker environment, I eventually found the one-line SDK fix that solved everything. Here it is for the AWS SDK v3 in JavaScript:
const client = new S3Client({
endpoint: "http://localhost:9000",
forcePathStyle: true,
region: "us-east-1",
credentials: {
accessKeyId: "minioadmin",
secretAccessKey: "minioadmin",
},
});
For the AWS SDK v2 (Python/boto3), the equivalent is:
s3 = boto3.client(
's3',
endpoint_url='http://localhost:9000',
config=Config(s3={'addressing_style': 'path'})
)
Local Development Environments
LocalStack, the popular local AWS emulator (free tier at around $0/month, pro tier starting around $35/month as of early 2025), defaults to path style on http://localhost:4566. Same configuration as MinIO above applies. Set forcePathStyle: true and point the endpoint at localhost. Done.
Common URL Errors and How to Fix Them
Probably should have opened with this section, honestly — it’s what most people are here for.
403 Forbidden — The Format Mismatch
You have permissions. The policy is correct. You’re still getting 403. Check whether your code is constructing a path-style URL for a post-2020 bucket. This is a format issue, not a permissions issue. AWS returns 403 rather than 404 or a more descriptive error, which makes it maddening to diagnose the first time.
Fix: Switch your SDK config or constructed URL to virtual hosted format. Verify the bucket was created after September 2020 and that it’s not one of the grandfathered buckets.
Region Mismatch — PermanentRedirect Errors
You’ll see this as an XML response with <Code>PermanentRedirect</Code> and a suggested endpoint. The bucket exists, you have access, but you’ve specified the wrong region in the URL. The bucket is in eu-west-1 and your code has us-east-1 hardcoded somewhere.
Fix: Check the actual bucket region in the S3 console or with aws s3api get-bucket-location --bucket your-bucket-name. Update the endpoint region to match. Don’t rely on redirects in production — they add latency and occasionally cause SDK retry loops.
Bucket Naming Restrictions for Virtual Hosted Style
Virtual hosted style has harder constraints on bucket names. The bucket name becomes a DNS subdomain label, which means:
- No uppercase letters —
MyDataBucketwill not work as a virtual hosted subdomain - No dots in the bucket name —
my.data.bucketcreates SSL certificate validation errors because it creates a multi-level subdomain (my.data.bucket.s3.amazonaws.com) that doesn’t match AWS wildcard certs - No underscores — these aren’t valid in DNS labels
- 3 to 63 characters, must start and end with lowercase letter or number
If your bucket name has dots, you’re essentially stuck with path style or a CloudFront distribution in front. The dots issue trips up teams that named buckets after internal domain names — something like data.internal.company — which seemed reasonable at the time.
SSL Errors on Dotted Bucket Names
The error looks like a certificate mismatch or ERR_CERT_COMMON_NAME_INVALID. The bucket name has dots. The virtual hosted URL would be my.dotted.bucket.s3.amazonaws.com and the wildcard cert *.s3.amazonaws.com doesn’t cover multiple subdomain levels. Use path style for these buckets or rename them if you can.
Quick Reference Table
Single table. Screenshot it, bookmark it, do whatever is useful.
| Format | URL Structure | When to Use | SDK Config Key | Example URL |
|---|---|---|---|---|
| Virtual Hosted | https://{bucket}.s3.{region}.amazonaws.com/{key} |
Default for all new AWS S3 buckets (post Sept 2020); anything on AWS proper | boto3: addressing_style: 'virtual' / SDK v3: forcePathStyle: false (default) |
https://my-bucket.s3.us-east-1.amazonaws.com/file.txt |
| Path Style | https://s3.{region}.amazonaws.com/{bucket}/{key} |
S3-compatible storage (MinIO, Ceph), LocalStack, legacy apps, dotted bucket names | boto3: addressing_style: 'path' / SDK v3: forcePathStyle: true |
https://s3.us-east-1.amazonaws.com/my-bucket/file.txt |
| Legacy Path (no region) | https://s3.amazonaws.com/{bucket}/{key} |
Don’t use — implicit us-east-1 only, deprecated | N/A — avoid | https://s3.amazonaws.com/my-bucket/file.txt |
The short version: use virtual hosted for anything on real AWS infrastructure. Use path style when you’re talking to something that isn’t AWS — or when you’re working around a dotted bucket name you can’t rename. Always include the region. Treat the legacy no-region format as something to search for and remove in old codebases, not something to build with.
Stay in the loop
Get the latest team aws updates delivered to your inbox.