S3: Durability, Classes and Access Control

What eleven nines actually promises, which storage class is a trap for small objects, and the four independent mechanisms that decide whether a request to a bucket succeeds.

intermediate 22 min lesson hands-on task included

S3 is the service most people think they already understand, and the one where the most expensive mistakes hide: a public bucket, a lifecycle rule that increases the bill, a delete that cannot be undone because versioning was never on.


Topic 1: The Object Model

S3 stores objects in buckets. An object is a key (up to 1,024 UTF-8 bytes), a value (0 bytes to 5 TB), metadata, a version ID, and access control information. There are no directories — logs/2026/08/app.log is a single flat key, and the console’s folder view is a rendering of key prefixes.

Facts with consequences:

  • Bucket names are globally unique across all AWS accounts. They are also DNS names, so lowercase, no underscores, 3–63 characters.
  • A bucket lives in one region. The name is global; the data is not, and it does not move.
  • Single-PUT maximum is 5 GB. Above that, multipart upload — which the CLI and SDKs do automatically above a threshold. Multipart also means failed uploads leave parts behind that bill until a lifecycle rule aborts them. That rule should exist in every bucket.
  • Strong read-after-write consistency for all operations, everywhere, since 2020. Material describing eventual consistency for overwrites and deletes is out of date; you no longer need to design around it.

Eleven nines of durability (99.999999999%) means the expected annual loss for 10 million objects is roughly one object every ten thousand years. It is a statement about hardware failure, achieved by replicating across at least three AZs, and it says nothing about the two things that actually delete your data: a bad aws s3 rm --recursive, and a compromised credential. Durability is not backup. Versioning, MFA delete, replication and Object Lock are backup.


Topic 2: Storage Classes

STORAGE CLASS = A TRADE BETWEEN STORAGE COST AND TIME-TO-FIRST-BYTE retrieval time → storage $/GB → Standard ms · no minimum Standard-IA ms · 30 days · retrieval fee One Zone-IA ms · single AZ Intelligent-Tiering ms · auto-moves · monitoring fee Glacier Instant ms · 90 days Glacier Flexible min–hours · 90 days Deep Archive 12h · 180 days WHEN IN DOUBT: INTELLIGENT-TIERING It beats guessing an access pattern you have not measured. MINIMUM DURATIONS ARE BILLED, NOT ENFORCED Lifecycle 1 KB logs into Glacier and you can pay more, not less.
Every class trades storage price against time-to-first-byte, and the minimum billable duration is the part that turns a sensible-looking lifecycle rule into a larger bill.
ClassRetrievalMin durationMin billable sizeUse
StandardmsnonenoneActive data
Intelligent-Tieringmsnonenone (monitoring fee per object)Unknown or changing patterns
Standard-IAms30 days128 KBKnown-infrequent, needs instant access
One Zone-IAms30 days128 KBReproducible data only — single AZ
Glacier Instant Retrievalms90 days128 KBArchives read a few times a year
Glacier Flexible Retrievalminutes–12h90 days40 KBReal archives
Glacier Deep Archive12h180 days40 KBCompliance retention

The two footguns are in the last two columns. Minimum duration is billed whether or not the object survives that long: transition a 7-day-lived log to Standard-IA and you pay 30 days for it. Minimum billable size rounds every object up: a 4 KB object in Standard-IA is billed as 128 KB — 32× the storage you are using, at a per-GB rate that is only 45% lower. Lifecycle rules on buckets full of small objects routinely increase costs, and the effect is invisible unless you look at object counts as well as bytes.

One Zone-IA is the class people misuse. It is 20% cheaper because it lives in a single AZ, which means an AZ loss destroys the data permanently. Correct only for data you can regenerate — thumbnails, derived datasets, a secondary copy of something stored elsewhere.

Intelligent-Tiering is the sane default when you do not know the access pattern. It moves objects between tiers automatically based on observed access, with no retrieval fees for the instant-access tiers, in exchange for a small per-object monitoring charge. That charge is why it is a poor fit for billions of tiny objects and an excellent fit for most everything else.

Storage Lens answers “what is actually in there” across every bucket — object counts by size, class distribution, incomplete multipart uploads, noncurrent version bytes. Read it before writing a single lifecycle rule.


Topic 3: Four Independent Access Mechanisms

A request to S3 is evaluated against four things, and any one of them can deny it:

1. Block Public Access        an account- and bucket-level override.
                              Wins over everything. Leave it ON.
2. Bucket policy              resource policy on the bucket
3. IAM identity policy        what the caller is permitted
4. Object ACLs                legacy; disable them

Block Public Access should be on at the account level, and switching it off should require a conversation. Four independent settings, all of which you want: block new public ACLs, ignore existing public ACLs, block new public bucket policies, restrict public buckets.

aws s3control put-public-access-block --account-id 111122223333 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Disable ACLs on every bucket by setting Object Ownership to BucketOwnerEnforced. ACLs are a pre-IAM mechanism that grants permissions per object, which means access can be granted in a place no policy review will ever look. With ACLs disabled, the bucket policy is the single source of truth — which is the property you want when someone asks “who can read this bucket”.

The bucket policy patterns worth having by default:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "DenyOutsideOrg",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
      "Condition": { "StringNotEquals": { "aws:PrincipalOrgID": "o-abc123example" } }
    }
  ]
}

Both are Denies, so no IAM policy written later can undo them. Remember the two-ARN pattern: bucket-level actions need the bucket ARN, object-level actions need bucket/*, and a policy with only one of them produces confusing partial failures.

Presigned URLs are how you grant time-limited access without changing any policy — the URL carries a signature made with the caller’s credentials, and it expires. Two properties people miss: the URL inherits the signer’s permissions (a presigned URL made with an admin role is an admin-scoped grant), and its maximum lifetime is bounded by the credentials that signed it — a role session that expires in an hour cannot produce a URL valid for a week.


Topic 4: Versioning, Replication and Object Lock

Versioning turns every overwrite and delete into a new version rather than a mutation. A DELETE inserts a delete marker; the previous version is still there. This is the single control that converts “someone deleted the bucket contents” from a disaster into an afternoon.

Two operational notes: versioning cannot be turned off once enabled, only suspended, and noncurrent versions bill at full price forever unless a lifecycle rule expires them. A bucket where versioning was enabled and no expiry rule was written is one of the most common sources of unexplained storage growth.

MFA Delete requires an MFA token to delete a version or change versioning state. It can only be enabled by the root user with the CLI, which makes it awkward — and appropriate for a small number of buckets holding audit logs or backups.

Replication copies objects to another bucket, same region (SRR) or cross region (CRR), possibly in another account.

  • Replication is asynchronous and applies to new objects only. Existing objects need Batch Replication.
  • Delete markers are not replicated by default. That is usually what you want for a backup copy — a deletion in the source does not propagate to the destination.
  • Replicating into a different account is the strong pattern: an attacker with credentials in the source account cannot reach the copy.
  • S3 Replication Time Control adds a 15-minute SLA and per-object metrics, which is what turns replication from “eventually” into something you can build an RPO on.

Object Lock provides WORM — write once, read many. In compliance mode, nobody can delete the object before the retention date, including the root user. That absoluteness is the point for regulatory retention, and the reason to test it in a sandbox first: there is no support case that undoes it.


Topic 5: Performance and Cost Behaviour

Request rates are per prefix: 3,500 PUT/COPY/POST/DELETE and 5,500 GET/HEAD per second per prefix, and prefixes scale horizontally without limit. The old advice about randomising key prefixes is obsolete — S3 partitions automatically — but keys that all share one prefix still share one prefix’s budget. Spreading a high-throughput workload across 2026/08/18/hh/ style prefixes is enough.

Three performance features worth knowing:

  • Multipart upload — parallelism for large objects, and resumability. Configure the CLI threshold rather than accepting defaults for a bulk migration.
  • S3 Transfer Acceleration — uploads enter through the nearest CloudFront edge and travel AWS’s backbone. Useful for global uploads over long distances, useless within a region, and billed extra.
  • S3 Select — SQL over a single object, retrieving only the matching rows. For anything beyond one object, Athena is the right tool.

Where the bill actually comes from is worth stating plainly, because it is rarely storage alone:

storage        $/GB/month, by class
requests       per 1,000 — PUT is ~10× the price of GET
data transfer  OUT to internet costs; IN is free;
               to another region costs; to another AZ inside a region is free
retrieval      IA and Glacier charge per GB retrieved
monitoring     Intelligent-Tiering, per object

A workload doing millions of tiny GETs can spend more on requests than on storage. A cross-region replication of a hot bucket can spend more on transfer than on either. Read the bill by line item, not by service total.


Topic 6: Static Hosting and CloudFront

S3 static website hosting serves a bucket over HTTP. It supports index and error documents, and it does not support HTTPS — which alone disqualifies it for anything public.

The correct pattern is CloudFront in front of a private bucket, with Origin Access Control signing CloudFront’s requests so the bucket can stay entirely closed to the internet:

{
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-site/*",
  "Condition": {
    "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1ABCDEF" }
  }
}

What that buys beyond TLS: caching at the edge (lower latency and far fewer origin requests), a free ACM certificate, WAF integration, and — the one people forget — cheaper egress, because CloudFront’s per-GB rate is lower than S3’s direct internet transfer and cache hits cost nothing at the origin.

Note that OAC is the current mechanism; Origin Access Identity is its predecessor and appears in most older material. OAI still works but does not support SSE-KMS origins or all HTTP methods, so new distributions should use OAC.

Try it yourself: enable versioning on a test bucket, upload a file twice, delete it, then list versions with aws s3api list-object-versions. Remove the delete marker and watch the object come back. Then check aws s3api list-object-versions --query 'Versions[].Size' | paste -sd+ | bc against what the console reports as bucket size — the gap is what noncurrent versions are costing you.

Common mistake: writing a lifecycle rule that transitions everything to Glacier after 30 days on a bucket of application logs. Small objects hit the 40 KB minimum, the 90-day minimum duration bills for objects deleted at day 45, and retrieval during an incident takes hours. Compress and aggregate logs first, then archive; or set a plain expiry and accept that logs older than the retention window have no value.