Words that inspire, ideas that spark,
stories that stay
What Happens Behind the Scenes When You Upload an Image to a CMS?
Pankaj Panday
Friday, September 11th, 20266 min read
Table of content
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.