Words that inspire, ideas that spark, stories that stay

What Happens Behind the Scenes When You Upload an Image to a CMS?

Pankaj Panday

Pankaj Panday

Friday, September 11th, 20266 min read

What Happens Behind the Scenes When You Upload an Image to a CMS?

What Happens Behind the Scenes When You Upload an Image to a CMS?

Every time I hit "Upload" on a CMS media panel, the file just... appears. Works. Nobody on my team has ever asked what happens in between. We treat it like a black box, mostly because the SDK abstracts it away and we're too busy wiring up getStaticProps to care.

But once you start building custom fields or debugging a broken upload in prod, you realize there's a whole pipeline hiding under that button.

Step 1: Upload Validation

The first thing the server does before touching disk (or S3) is validate the incoming file. A few checks I've seen across CMSs:

  • MIME type validation — not just trusting the file extension, actually sniffing the file header/magic bytes. .jpg renamed to .exe shouldn't fly.

  • File size limits — usually configurable, rejected early to avoid wasting bandwidth/storage.

  • Security checks — Handling EXIF metadata varies by platform. Some image processing pipelines preserve it, while others strip or ignore parts of it during optimization depending on configuration and privacy requirements.

  • Duplicate detection — some systems hash the file (checksum, usually SHA-256 or MD5) and check against existing records before writing anything new.

 



Step 2: Asset Storage

Once validated, the binary and the metadata split paths.

  • The actual image binary almost never lands in the database. Databases are bad at storing large blobs efficiently — bloats backups, slows queries.

  • Instead it goes to object storage — think AWS S3, or S3-compatible stuff like Cloudflare R2, GCS, MinIO for self-hosted setups.

  • The database only keeps metadata: filename, mime type, dimensions, alt text, the storage key/URL.

S3 example, roughly how this looks in practice:

Upload handler gets the validated buffer, does something like:

// binary → object storage

await s3.putObject({

 Bucket: 'my-cms-assets',

 Key: `uploads/${hash}-${filename}`,

 Body: fileBuffer,

 ContentType: mimeType,

})

 

// metadata → database

{

 "filename": "hero-banner.jpg",

 "mimeType": "image/jpeg",

 "width": 1920,

 "height": 1080,

 "url": "<https://my-cms-assets.s3.amazonaws.com/uploads/abc123-hero-banner.jpg>",

 "sizeBytes": 482113

}

 

This split matters for a practical reason: object storage is cheap and built to serve large files over HTTP directly. A database isn't designed for that job, and querying metadata is a lot faster when the row is small.

Different CMSs wire this up differently:

  • Payload / Strapi — you configure your own storage adapter (local disk, S3, GCS), metadata lives in whatever database you connect.

  • Sanity — assets go into Sanity's own asset store, which is already CDN-backed. You don't point it at your own bucket.

  • Directus — sits on top of your own database plus a storage adapter, follows a similar self-hosted storage model compared to Strapi, although implementation details differ.

  • Builder.io — follows a managed approach similar to Sanity. Uploaded assets are served through Builder's CDN-backed Image API, where image variants can be generated using URL parameters for resizing, quality, and format conversion rather than configuring your own storage pipeline.