Module A-11·30 min read

Object storage semantics, pre-signed uploads that keep bytes out of your app tier, transcoding pipelines, and CDN delivery with signed URLs.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module A-11 — Blob Storage and Media Pipelines

What this module covers: Why large bytes do not belong in your database, stated as a cost rather than a preference. Object storage semantics and how they differ from a filesystem in ways that break assumptions — a flat keyspace, no rename, whole-object writes, and the consistency guarantees you should and should not design on. Key design, including the content-addressed scheme that eliminates cache invalidation entirely. Then the single most important pattern in the module: pre-signed uploads that keep gigabytes out of your application tier, with the constraints that must live inside the signature and the reason you can never trust the client's "I'm done" notification. Validation and the specific attacks that arrive as files — decompression bombs, executable SVGs, and EXIF location data. The media pipeline as a job DAG, segment-parallel transcoding, and treating derivatives as a cache over an immutable original. Delivery through a CDN with signed URLs, and why HLS breaks the per-object signing model. Finally storage tiering, where the obvious saving is often a loss.


Bytes Do Not Belong in the Database

The temptation is real: a bytea column keeps everything transactional, gives you one backup, and removes a dependency. The bill for a 4 GB video in a Postgres column:

  • Every byte enters the WAL, so it is written twice locally and shipped to every replica and every CDC consumer (Modules P-5, P-23). Replication lag becomes a function of upload volume.
  • Backups grow accordingly. A 40 GB database of metadata becomes a 4 TB database, and your restore time — your RTO (Module O-7) — grows with it.
  • The bytes pass through the driver's memory on both write and read, so a handful of concurrent downloads is an out-of-memory kill.
  • A connection is held for the duration of the transfer (Module P-4), so a slow client on a mobile network occupies a database connection for minutes.
  • You pay OLTP storage prices — provisioned IOPS, replicated, high-availability — for bytes that are read once a month (Module O-8).

The rule is clean and worth stating as a rule:

The database stores the metadata and the pointer. The object store stores the bytes.

Small binaries are a legitimate exception — an avatar under 100 KB, a PDF under a megabyte — where transactional simplicity genuinely beats a second system. The threshold is not a magic number; it is where the costs above start to matter, which in practice is somewhere in the low hundreds of kilobytes and a modest total volume.

Analogy: a library's card catalogue and its stacks. The catalogue is small, indexed, updated transactionally, and searched constantly; the books are large, cold, and retrieved by reference. Nobody photocopies every book into the catalogue drawer to keep them together — and the reason is not tidiness, it is that the drawer's properties (fast, indexed, replicated to every branch) are expensive per gram and wasted on paper nobody is searching.


Object Storage Is Not a Filesystem, and the Differences Bite

Every difference below has produced a production bug in somebody's system.

The keyspace is flat. photos/2026/07/img.jpg is a single key containing slashes; there are no directories. "Listing a folder" is a prefix query with a delimiter, and it is a paginated API call whose cost grows with the number of objects, not with the number of "folders." So:

  • There is no rename or move. Renaming is COPY then DELETE, which is O(size) and not atomic. "Reorganise our folder structure" is a data migration, not a metadata operation.
  • There is no atomic directory operation at all — no atomic move of a prefix, no transactional multi-object write.

Writes are whole-object. You cannot modify byte 400 of a 2 GB file. Changing anything means uploading the whole object again (or using multipart upload for large ones). Appending is generally unavailable.

Consistency is better than it used to be, and not uniform. S3 has been strongly read-after-write consistent for PUTs and DELETEs since late 2020 — a genuinely important change that invalidated a decade of workarounds. But LIST and cross-region replication are not immediately consistent, and other providers differ. So: it is safe to read an object you just wrote by key; it is not safe to design a workflow around "list the prefix and see what appeared." Drive workflows from a database row or an event, never from a listing.

Conditional writes now exist, and they change what is possible. S3 supports If-None-Match (write only if absent) and conditional overwrite, which gives you optimistic concurrency and — relevant to Module A-2 — makes object storage fenceable, where for years it was the canonical example of a resource you could not fence. If you have a design that assumed otherwise, check your provider's current capabilities.

Latency and cost profile. First-byte latency is tens of milliseconds, throughput is enormous and scales with parallelism, and pricing has three axes: per-GB stored, per-request, and per-GB egress. That combination makes it an excellent bulk store and a poor database — and it makes millions of tiny objects surprisingly expensive, because per-request charges dominate when the objects are small.

Durability is not availability, and neither is backup. Eleven nines of durability means the provider will not lose your bytes. It does not stop you deleting them, a bug overwriting them, or ransomware encrypting them. Versioning plus object lock (write-once-read-many) is the backup, and it is the mechanism Module O-7 relies on for immutable copies.


Key Design: Content-Addressing Solves Cache Invalidation

Two decisions here have outsized consequences.

Distribute the prefix if your write rate is high. Object stores partition by key prefix, so sequential keys concentrate load on one partition — the same hot-partition mechanism as Module P-6, except here you want scatter, which is the mirror image of Module P-3's argument for time-ordered database keys. S3 now auto-scales prefixes (thousands of requests per second per prefix), so this matters only at genuinely high rates; when it does, a short hash prefix fixes it.

Make keys content-addressed and immutable. Name the object by the hash of its contents:

objects/sha256/9f/86/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Then keep the logical name in your database:

sql

What this buys is disproportionate:

  • Cache invalidation disappears. An immutable object can be served with Cache-Control: public, max-age=31536000, immutable, cached forever at every layer, and never purged — because a change produces a different key. Modules F-12 and P-13 spent considerable effort on invalidation; content addressing removes the problem rather than solving it.
  • Deduplication is free. The same file uploaded by a thousand users is one object.
  • Integrity is verifiable. The key is the checksum.
  • Rollback is a metadata update. Point the row at the previous key.

The cost is a level of indirection and a garbage-collection problem: an object is unreferenced only when no row points at it, so deletion needs reference counting or a mark-and-sweep job.

And never put user-controlled strings directly into keys. Path traversal (../../), Unicode normalisation differences, case sensitivity, length limits and reserved characters all become bugs. Generate the key yourself and store the user's filename as metadata.


Pre-Signed Uploads: Keep the Bytes Out of Your Application Tier

This is the most important pattern in the module. A 2 GB upload proxied through your API consumes a request slot for minutes, buffers to memory or disk, occupies a connection, and costs you inbound bandwidth and outbound bandwidth to the store — for bytes you do nothing with.

Instead, let the client talk to the object store directly, using a short-lived credential you issue:

text

Two details make the difference between this being safe and being an open file-drop.

The constraints must be inside the signature. A pre-signed URL without them lets the client upload any size, any content type, to any key:

ts

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.