S3 bucket design looks like a naming exercise and turns out to be an architecture decision. A bucket is the unit at which most S3 controls are applied: access policy, versioning, Object Lock, lifecycle rules, replication and logging are all set per bucket. How data is divided into buckets therefore decides how finely those controls can be applied, and how much administrative work it takes to apply them consistently. One bucket for everything is simple until two data sets need different rules. A bucket per application per environment per team is precise until there are four thousand of them.
This post sets out the factors that should drive the number of buckets, the patterns that work at scale and the signs that a layout needs revisiting.
The starting point is to list which settings exist only at bucket level, because those are the ones a layout cannot work around later. The table below covers the settings that most often force data into separate buckets.
| Setting | Scope | Consequence for layout |
|---|---|---|
| Versioning | Whole bucket | Data that must not keep old versions needs its own bucket |
| Object Lock and default retention | Whole bucket, with per-object overrides | Immutable and mutable data are usually separated |
| Bucket policy | Whole bucket, with prefix conditions | Simple policies favor one audience per bucket |
| Lifecycle rules | Bucket, filtered by prefix or tag | Can share a bucket if prefixes are disciplined |
| Replication | Bucket, filtered by prefix or tag | Data with different destinations is easier to manage apart |
| Access logging and metrics | Bucket | Reporting and chargeback follow bucket lines |
Two rows stand out. Versioning and Object Lock change how deletion behaves for everything in the bucket, so mixing data that should be immutable with data that should be freely deleted leads either to retained data nobody wanted or to unprotected data that should have been locked. The interaction between versioning and storage growth is covered in the post on S3 versioning costs.
Inside a bucket, key prefixes give the appearance of folders, and many controls can be filtered by prefix. That makes it tempting to use one large bucket with a prefix per application. It works for lifecycle rules and for read access, and it has limits. A bucket policy that grants different rights to twenty prefixes becomes long, hard to review and easy to get wrong, and a single mistake exposes data across teams. Prefix-based separation also depends on every writer following the naming convention, with nothing in the storage system to enforce it.
The reverse mistake is treating buckets as folders, creating one per customer, per day or per job. Each bucket carries its own configuration, and operations that span buckets, such as applying a new policy or checking that logging is enabled, have to be repeated for each. The post on S3 access policies describes how policy sprawl develops and how to contain it.
Three patterns cover most estates. The first is a bucket per application and environment: production, test and development data for each application live apart, with access granted to that application's role. It maps cleanly onto ownership and keeps a mistake in test from touching production. The second is a bucket per data class within an application, used when one application writes data with different retention, such as logs kept for 30 days and records kept for seven years. The third is a bucket per tenant, used by service providers and internal platforms where each customer or business unit must be isolated and billed separately.
These combine. A reasonable rule is that two data sets belong in separate buckets when they differ in owner, in retention or immutability, in who may read them or in where they are replicated. If none of those differ, a prefix is enough.
On multi-tenant object storage, the account is a stronger boundary than the bucket. Each account has its own users, keys and policies, and by default nothing in one account can see another's buckets. Where teams or customers must be isolated from each other, separate accounts provide that with less policy work than many buckets under one account. Buckets then divide data within an account by application and data class. Designing the account structure first and the bucket structure second avoids using bucket policies to do a job that accounts do better.
The number of objects per bucket matters more than the number of buckets. A bucket holding billions of objects is slow to list, and operations that enumerate it, such as inventory, lifecycle evaluation and migration, take correspondingly long. Splitting a very large data set across several buckets along a natural line, such as by year or by source system, keeps each one manageable. The post on object storage metadata explains why listing cost grows with object count.
Bucket count has its own limits. Public cloud services cap the number of buckets per account by default, and on any platform thousands of buckets become difficult to audit. Designs that create a bucket per job or per user tend to hit that wall. If a design needs that many, key prefixes or object tags inside a smaller number of buckets are usually a better fit.
Bucket names are hard to change, since renaming means copying every object. A convention that encodes owner, application, environment and data class, in that order, makes buckets sortable and lets policies and automation match on patterns. Names should avoid anything that will change, such as a project code name or a team name that may be reorganized away.
Buckets also need an end of life. Each should have a recorded owner and purpose, and empty or abandoned buckets should be reviewed periodically. An unowned bucket with a permissive policy is a common source of exposure. Lifecycle rules, covered in S3 lifecycle policies, handle the objects. Something has to handle the containers.
Scality RING exposes an S3 interface and is multi-tenant, with accounts, buckets and policy-based isolation, so the account-then-bucket structure described here can be applied directly. RING supports versioning and S3 Object Lock, which are set per bucket, so separating immutable data from mutable data is a layout decision the platform enforces once made.
RING emits operational metrics and logs that the operator keeps locally, so object counts, growth and request rates can be tracked per bucket as a layout is put under load. It distributes data across the nodes in the system, spreading request load across many devices.
The right number of buckets is the smallest number that lets each data set have the access, retention and replication rules it needs. Working that out before the first application goes live is far cheaper than copying petabytes into a new layout later.