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.

Step 3: Image Optimization

Storing the original file is only half the job. The original is usually the wrong version to actually serve — it might be a 6000×4000 photo straight off someone's phone, and no browser needs that at full resolution for a thumbnail.

This is where the transformation pipeline kicks in. The CMS (or a service it delegates to) generates additional versions of the image:

  • Resized dimensions — thumbnail, medium, large

  • Format conversion — original JPEG/PNG converted into WebP or AVIF

  • Compression — reducing file size while keeping visual quality acceptable

WebP and AVIF matter here because they compress noticeably better than JPEG/PNG at similar visual quality, which means smaller payloads and faster page loads. AVIF tends to compress even tighter than WebP but isn't supported quite as universally across older browsers, which is why most pipelines generate both and let content negotiation pick the right one.

These generated versions are called image variants — a single upload can produce a dozen or more derivative files, each with its own asset URL.

On-Demand vs Pre-Generated Transformations

There are two common approaches to when these variants actually get created:

Pre-generated — the CMS creates a fixed set of sizes/formats right after upload, based on a config you define upfront. Predictable, but inflexible if you need a size nobody predicted.

On-demand — the CMS generates a variant the first time it's requested, using query parameters in the asset URL (something like ?w=800&fmt=webp), then caches the result so it doesn't regenerate on every request.

On-demand tends to be more common in newer platforms because frontend needs change constantly, and pre-generating every possible size/format combination up front just isn't practical. Sanity and Builder.io both expose URL-based image transformation APIs, where parameters such as width, height, quality, and format determine the generated image variant. This gives frontend developers flexibility without requiring every possible image size to be generated during upload.

Step 4: CDN Delivery

Once a variant exists, it doesn't get served straight from object storage on every request — that would be slow for anyone far from the storage region and would generate unnecessary load.

Instead, it goes through a content delivery network (CDN). The CDN sits between the browser and the origin (object storage or the CMS's image service) and keeps copies of frequently requested assets at edge locations closer to the user.

The first request for a given variant is a cache miss — the CDN fetches it from origin, caches it, then serves it. Every subsequent request for that same asset URL is a cache hit, served directly from the edge without touching origin again. The cache key is typically derived from the asset URL, including transformation parameters.

This is why asset URLs from a CMS often include a hash or version identifier — it lets the CDN and browser cache the file aggressively (long Cache-Control max-age) without worrying about stale content, because a changed image gets a new URL entirely rather than overwriting the old one.

Step 5: Responsive Images and Browser Rendering

On the frontend, none of this is useful unless the browser actually picks the right variant. This is where responsive images come in — using srcset and sizes attributes (or Next.js's <Image> component, which generates these automatically) to tell the browser: here are several versions of this image, pick whichever fits the viewport and pixel density.

<img

 srcset="

   image-400.webp 400w,

   image-800.webp 800w,

   image-1600.webp 1600w

 "

 sizes="(max-width: 600px) 400px, 800px"

 src="image-800.webp"

 alt="Hero banner"

/>

 

The browser evaluates sizes, matches it against srcset, and requests only the variant it actually needs — a phone doesn't download the 1600w version.

Lazy loading (loading="lazy") delays fetching offscreen images until the user scrolls near them, which reduces initial page weight without changing what eventually renders.

Once the right variant is chosen, standard browser rendering takes over — decode the image, lay out the box, paint it. Nothing CMS-specific happens at this point; by now it's just an image being loaded like any other.

Conclusion: Why This Pipeline Exists

None of this complexity is there for its own sake. Every stage solves a specific problem: validation stops bad or malicious files from ever getting stored, splitting binary from metadata keeps storage and queries fast, generating variants avoids shipping oversized images, and CDN caching avoids hammering origin storage on every page view. Skip any one stage and you either get a security hole, a slow database, a slow page, or a slow origin server under load.

One thing I didn't appreciate until debugging production uploads is that different CMS platforms expose different parts of this pipeline. Some abstract almost everything behind a managed service, while others let you configure storage adapters, transformation libraries, and CDN behavior yourself. The underlying flow stays largely the same, but the amount of control you get varies quite a bit.

Once you see the full chain, the "just works" upload button stops feeling like magic and starts looking like a fairly standard set of tradeoffs — the same ones you'd make if you were building this yourself.

 

Booking form image

Struggling to choose the right Headless CMS & Headless Commerce tech stack?
 We’ll help you pick the best solution for your business! Exclusive Offer: 20 Hours of Free Development & Consultation


Book a Meeting

Related articles

You may also like

Top 6 Drupal Alternatives 2026

Top 6 Drupal Alternatives 2026

Read more
Author

Ardra Pillai

Monday, March 31st, 2025

7 min read
Top 5 best Directus CMS agency

Top 5 best Directus CMS agency

Read more
Author

Vipul Uthaiah

Sunday, June 22nd, 2025

5 min read
Case Study: How Mekanika.io Eliminated Shopify Multi-Store Costs and Scaled Globally

Case Study: How Mekanika.io Eliminated Shopify Multi-Store Costs and Scaled Globally

Read more
Author

Vipul Uthaiah

Friday, August 8th, 2025

5 min read