Filter concepts by levelShowing all levels.

System Design · Section 45

Object Storage

Level
intermediate
Read
18 min
Concepts
3

S3-style object storage holds data as whole, immutable objects addressed by a key inside a flat-namespace bucket, each carrying metadata alongside its bytes — "folders" are only a prefix illusion over that flat namespace. Versioning keeps every past write or delete as a distinct version instead of overwriting in place (a DELETE inserts a marker rather than erasing data), and lifecycle rules automate what happens to those objects and versions over time, transitioning storage class or expiring them on a schedule with no application code involved. Two operational mechanics do the heavy lifting day to day: multipart upload splits a large object into independently-retriable parts (recommended at 100 MB or larger) that must be explicitly completed or aborted, and signed URLs grant one caller's time-limited, action-scoped access to a single object without ever sharing credentials. Together these make object storage the right place for large durable files — routing them directly to the object store instead of relaying them through an application server turns a file-size-dependent cost on that server into a small, roughly constant one, and moves durability onto a system built for it.

This section

What is true here

  1. A bucket is a flat namespace; a key is an object's full identity — there are no real folders, only shared key prefixes.
  2. Versioning keeps every version; DELETE inserts a marker rather than erasing data — pair it with a lifecycle rule that expires noncurrent versions, or storage cost grows unbounded.
  3. Multipart upload (parts 1-10,000, recommended at 100 MB+) must be explicitly completed or aborted — an abandoned upload keeps billing for its parts.
  4. A signed URL grants one caller's time-limited, action-scoped access without sharing credentials, and cannot be individually revoked before it expires.
  5. Route large durable files directly to object storage instead of relaying them through an application server — the app server's job shrinks to recording metadata.

What you will be able to do

  • Explain what a bucket, key, and object's metadata are, and why S3 "folders" are not real directories
  • Predict what versioning and a DELETE actually do to stored data, and pair versioning with a lifecycle rule correctly
  • Walk through the create/upload-parts/complete shape of a multipart upload and why incomplete uploads need explicit cleanup
  • Use a signed URL to grant temporary, scoped access to an object without sharing credentials
  • Decide when to route a large file directly to object storage instead of through an application server

The object storage data model

Buckets, keys, metadata, versioning and lifecycle rules — what an object is and how it changes over time.

The object storage model: buckets, keys, metadata, versioning

coreintermediate

Object storage holds data as whole, immutable objects — not files in a directory tree and not rows in a table. A bucket is a flat namespace; every object inside it is addressed by a key, a string that is the object's entire identity (there are no real subdirectories, only keys that share a prefix). Each object carries metadata alongside its bytes — content type, custom headers, timestamps. Versioning, once turned on for a bucket, keeps every past copy of an object instead of overwriting it on write or delete. Lifecycle rules then automate what happens to those objects and versions over time: moving them to cheaper storage tiers or expiring them, based on age or prefix, with no application code involved.

Think of it as

Think of a bucket as a warehouse that only stores sealed, labeled boxes — never loose paper you can edit in place. The key is the barcode stuck on the box: read it and you get the box, but there is no shelf structure the warehouse actually enforces, just barcodes that happen to share a prefix like "invoices/2026/". The metadata is the shipping label on the outside of the box (what's inside, when it arrived) — readable without opening the box. Versioning is the warehouse keeping every previous box that ever had that barcode instead of throwing the old one out when a new one arrives. Lifecycle rules are a standing instruction taped to the warehouse wall — "move anything untouched for 90 days to the cheap back room; shred anything untouched for a year" — that the warehouse staff carries out on a schedule, with nobody having to ask them each time.

text
bucket:      my-app-uploads
key:         invoices/2026/inv-4471.pdf
metadata:    Content-Type: application/pdf, uploaded-by: svc-billing
version id:  a1b2c3...   (only exists once versioning is enabled)

What we're doing: See what a DELETE actually does once versioning is enabled, and how a lifecycle rule cleans up what it leaves behind.

versioning-and-lifecycle.txttext
1  # Bucket "invoices" has versioning ENABLED.
2
3  PUT  invoices/2026/inv-4471.pdf   -> version v1 (current)
4  PUT  invoices/2026/inv-4471.pdf   -> version v2 (current), v1 still stored
5  DELETE invoices/2026/inv-4471.pdf -> delete marker (current), v1 + v2 still stored
6
7  # GET on the key now returns "not found" (the delete
8  # marker is current) — but v1 and v2 are not gone, and
9  # both are still billed as stored objects.
10
11 # A lifecycle rule fixes the storage-cost side of this:
12 #   expire noncurrent versions after 30 days
13 #   -> v1 and v2 are permanently deleted 30 days after
14 #      each stopped being the current version
5
The DELETE does not remove v1 or v2 — it adds a new "delete marker" version that becomes current, which is why GET now reports not-found even though data still exists in the bucket.
9
Both prior versions are still stored and still billed until something explicitly removes them — versioning trades "delete is reversible" for "delete alone does not reduce storage cost."
12
A lifecycle rule targeting noncurrent versions is what actually reclaims that storage automatically, on a schedule, with no application code involved.

Why this works: Versioning and lifecycle rules are usually adopted together for exactly this reason — versioning alone protects against accidental overwrite/delete but silently grows storage cost forever unless a lifecycle rule is paired with it to expire old versions.

Enabling versioning without a matching lifecycle rule

Wrong

text
"We turned on versioning for safety — nothing
else to configure."

Better

text
"We turned on versioning, and added a lifecycle
rule to expire noncurrent versions after N days
— otherwise every overwrite and delete just
keeps growing storage cost forever."

What you see: Storage cost for a bucket climbs steadily even though the number of "current" objects looks stable — because every overwrite and delete is silently retained as a noncurrent version with no expiration.

Why: Versioning has no built-in cleanup — it is a pure "keep everything" mechanism until a lifecycle rule (or manual deletion) removes noncurrent versions, so the two are almost always configured as a pair, not versioning alone.

Versioned writes and a DELETE, on one key
Client
Bucket
  1. 1. PUT inv-4471.pdfcreates v1 (current)
  2. 2. PUT inv-4471.pdfcreates v2 (current), v1 kept
  3. 3. DELETE inv-4471.pdfadds a delete marker (current) — v1, v2 still stored and billed
  1. Client → Bucket: PUT inv-4471.pdf (creates v1 (current))
  2. Client → Bucket: PUT inv-4471.pdf (creates v2 (current), v1 kept)
  3. Client → Bucket: DELETE inv-4471.pdf (adds a delete marker (current) — v1, v2 still stored and billed)

The four core concepts of the object storage model

The four core concepts of the object storage model
ConceptWhat it isKey property
BucketA flat, named container for objectsGlobally unique name; holds unlimited objects
KeyThe unique string identifying an object within a bucketNo real folders — shared prefixes only look like directories
MetadataData about the object, stored alongside itReadable without fetching the object body itself
VersioningKeeps every write/delete as a distinct versionA DELETE inserts a marker; it does not erase prior versions

Remember: Bucket = flat container, key = full object identity (no real folders, only shared prefixes), metadata = data about the object stored alongside it. Versioning keeps every version; DELETE adds a marker rather than erasing data. Lifecycle rules are what actually reclaim storage from old versions — configure them together, not versioning alone.

See also: multipart upload and signed urls · object storage vs application servers

Advertisement

Uploading and granting access

The two mechanics that do the day-to-day work: splitting large uploads into parts, and granting temporary access without sharing credentials.

Multipart upload and signed URLs

coreintermediate

Multipart upload splits one large object into independently-uploaded parts (numbered 1 to 10,000) that the storage service reassembles once all parts arrive — AWS recommends it for objects 100 MB or larger. It lets parts upload in parallel, lets a failed part retry alone instead of restarting the whole object, and requires an explicit "complete" call at the end — an upload that is never completed or aborted just sits there, still billed for storage. A signed URL (also called a presigned URL) is a plain URL with a cryptographic signature embedded in it that grants time-limited permission to GET or PUT a specific object, generated using one specific user or role's credentials, without that caller ever handling or sharing those credentials.

Think of it as

Multipart upload is like moving house by shipping numbered boxes separately instead of one giant truck: each box can travel its own route, a lost or damaged box gets reshipped alone without repacking the whole house, and nothing is usable at the destination until you send the final "all boxes have arrived, unpack them in order" confirmation — and if you never send that confirmation, the boxes just sit in the mover's warehouse racking up storage fees. A signed URL is like a claim ticket a hotel concierge hands a guest for one suitcase: the ticket itself proves the bearer may pick up exactly that bag, for a limited window of time, without the concierge ever handing over their own staff badge.

bash
# Generate a presigned URL valid for 1 hour (CLI: max 7 days)
aws s3 presign s3://my-bucket/reports/q3.pdf --expires-in 3600

What we're doing: Walk through the three-call shape of a multipart upload and why an abandoned one keeps costing money.

multipart-upload.txttext
1  CreateMultipartUpload  bucket=videos key=lecture-09.mp4
2    -> returns upload_id = "abc123"
3
4  UploadPart  upload_id=abc123 part_number=1  (100 MB)
5  UploadPart  upload_id=abc123 part_number=2  (100 MB)
6  UploadPart  upload_id=abc123 part_number=3  (42 MB)
7    -> each call can run in parallel, retry alone on failure
8
9  CompleteMultipartUpload  upload_id=abc123 parts=[1,2,3]
10   -> S3 assembles the object; parts no longer billed separately
11
12 # If step 9 never happens (client crash, app bug):
13 # parts 1-3 remain stored and billed indefinitely, until
14 # something explicitly calls AbortMultipartUpload or a
15 # lifecycle rule targeting incomplete uploads cleans it up.
1
CreateMultipartUpload is the first of three required calls — it returns an upload_id that ties every subsequent part to this one logical object.
6
Parts can be different sizes and uploaded in parallel or out of order — S3 reassembles them by part number, not by arrival order.
13
Skipping CompleteMultipartUpload (or AbortMultipartUpload) leaves the uploaded parts in storage, billed, with no object ever becoming visible to a GET — this is the operational trap, not a hypothetical one.

Why this works: The three-call shape — create, upload N parts, complete — is what makes multipart upload resumable and parallel, but that same explicitness is exactly what makes an abandoned upload a real, silent cost if nothing ever completes or aborts it.

Never cleaning up incomplete multipart uploads

Wrong

text
"Our upload flow calls CreateMultipartUpload
and UploadPart — if the user closes the tab
before we call Complete, no object was created,
so there's nothing to worry about."

Better

text
"We configure a lifecycle rule to abort
incomplete multipart uploads after N days, so
abandoned uploads (crashed clients, closed tabs)
don't silently accumulate storage cost forever."

What you see: A bucket's storage bill is noticeably higher than the sum of its visible objects' sizes would suggest — because dozens or hundreds of interrupted uploads never called Complete or Abort, and their parts are still being billed.

Why: S3 has no automatic timeout on an in-progress multipart upload — it is deliberately left open-ended so a slow or resumable upload is never cut off mid-transfer, which means cleanup is the caller's responsibility, typically delegated to a lifecycle rule rather than trusted to always happen in application code.

Multipart upload: create, N parts, complete
uploadall arrivenevercompleted

CreateMultipartUpload

returns upload_id

3 parts

parallel, retry alone on failure

CompleteMultipartUpload

object assembled

Abandoned

billed indefinitely until aborted

  • CreateMultipartUpload — returns upload_id
    • leads to 3 parts (upload)
  • 3 parts — parallel, retry alone on failure
    • leads to CompleteMultipartUpload (all arrive)
    • on error, leads to Abandoned (never completed)
  • CompleteMultipartUpload — object assembled
  • Abandoned — billed indefinitely until aborted

Multipart upload vs a single PUT

Multipart upload vs a single PUT
PropertySingle PUTMultipart upload
Recommended forObjects under ~100 MBObjects 100 MB or larger
ParallelismOne request, one streamParts upload independently, in parallel
Failure recoveryWhole object restarts on failureOnly the failed part needs retrying
Cleanup if abandonedNothing to clean up — it just failedMust explicitly complete or abort, or parts are billed indefinitely

Remember: Multipart upload: parts 1-10,000, recommended at 100 MB+, must be explicitly completed or aborted (or its parts are billed forever). Signed URL: one caller's time-limited, action-scoped access embedded in a URL — cannot be individually revoked, only left to expire or blocked by revoking the signer's own credentials.

See also: the object storage model · object storage vs application servers

Advertisement

Why object storage instead of the application server

The concrete before/after of routing a large file through an app server vs directly to object storage.

Object storage instead of application servers for large files

standardintermediate

When an application server sits between a client and a large file — accepting the upload, holding it in memory or on local disk, then writing it somewhere durable — every byte of that file consumes the app server's CPU, memory and network bandwidth, and the file vanishes if that server crashes before it's persisted elsewhere. Routing large durable files (videos, PDFs, backups, user uploads) directly to object storage instead removes the app server from that data path entirely: the client uploads straight to the object store (often via a signed URL so the app server never touches the bytes at all), and the app server's job shrinks to just recording metadata — the key, size, owner — in its own database.

Think of it as

An application server acting as a relay for large files is like a small shop where every delivery has to be carried through the cashier's counter before it reaches the storeroom — the cashier is now blocked handling a heavy box instead of ringing up customers, and if the cashier trips, the box might not make it to the storeroom at all. Routing large files directly to object storage is like having the delivery truck drive straight to the storeroom's own loading dock — the cashier just gets a note afterward saying "package #4471 arrived," and never had to touch or be blocked by the box itself.

What we're doing: Compare the request path for a 2 GB video upload through an app server vs directly to object storage.

upload-paths.txttext
1  # BEFORE: client -> app server -> object storage
2  Client  --2 GB POST-->  App server  --2 GB PUT-->  Object storage
3  # App server holds the full 2 GB (memory or temp disk)
4  # while relaying it onward; that request thread/worker
5  # is occupied for the entire transfer duration.
6
7  # AFTER: client -> object storage directly
8  Client  --GET-->  App server            (asks for permission)
9  App server  --signed URL-->  Client     (short-lived, scoped to one key)
10 Client  --2 GB PUT-->  Object storage    (direct, app server not involved)
11 Client  --"done, here's the key"-->  App server
12 App server  writes 200 bytes of metadata to its own database
13 # The app server never touches the 2 GB payload at all.
2
In the before path, all 2 GB physically flows through the app server — it is doing double the transfer work of a direct upload (receive, then forward).
9
The app server's only involvement in the after path is issuing a short-lived, single-key signed URL — a small, fast operation regardless of how large the eventual file is.
12
The app server's real job — recording metadata — is now decoupled from file size entirely; a 2 GB video and a 20 KB thumbnail cost the app server the same amount of work to record.

Why this works: This is the concrete shape of the architectural decision: it is not "object storage is generically better," it is that removing the app server from the data path for large files turns a variable, file-size-dependent cost into a small, constant one, while durability moves to a system built for it instead of depending on one server surviving until it can persist the file elsewhere.

A 2 GB upload: relayed vs. direct-to-object-storage

App server relays it

  • +Full 2 GB flows through the app server
  • +Occupies a thread/worker for the whole transfer
  • +A crash mid-transfer can lose the file

Direct to object storage

  • App server only issues a signed URL
  • Client uploads 2 GB straight to the store
  • App server records ~200 bytes of metadata
  • App server relays it
    • Full 2 GB flows through the app server
    • Occupies a thread/worker for the whole transfer
    • A crash mid-transfer can lose the file
  • Direct to object storage
    • App server only issues a signed URL
    • Client uploads 2 GB straight to the store
    • App server records ~200 bytes of metadata

Before (app server relays the file) vs after (direct-to-object-storage)

Before (app server relays the file) vs after (direct-to-object-storage)
AspectApp server relays the fileDirect to object storage
App server load per uploadScales with file size (memory/disk/bandwidth)Roughly constant — only issues a signed URL and records metadata
Failure mid-transferApp server crash can lose an in-flight fileObject store's own durability applies from the first byte it receives
Scaling large uploadsRequires scaling app server fleet capacity tooObject store scales independently of app server fleet size
What the app server storesThe file bytes themselves (at least transiently)Only a reference — key, size, owner — in its own database

Remember: Large durable files should not flow through the application server if avoidable — let the client upload directly to object storage (typically via a signed URL) and reduce the app server's job to recording metadata. This turns a file-size-dependent cost on the app server into a roughly constant one, and moves durability onto a system built for it.

See also: the object storage model · multipart upload and signed urls

Advertisement