Media & CDN

Image delivery is a CDN problem, and storage buckets don't solve it

A raw file in a bucket is not a fast image on a page. Draftbase is a media CDN built into the CMS: it uploads, processes, and serves images through a CDN with explicit cache headers. Storage answers "where is the file." A CDN answers "how fast does it reach the browser." Most media setups leave that second question unanswered.

Updated

Ready to simulate an upload

Why media delivery is a performance problem before it is a storage one

"Media management" often gets scoped as an upload form and a database record. That framing misses the part that actually affects visitors: how the resulting file reaches a browser, and how long it takes to get there.

Storing an image is the easy part. Serving it fast, at the right size, on every request is where most pipelines fall down. Images account for roughly 60-70% of the average webpage's total weight. (Source) That makes images the biggest single lever on page performance.

Largest Contentful Paint is usually an image. So how that image gets processed and cached moves the metric directly. Sites that treat media delivery seriously see it in their Core Web Vitals.

A CMS that only stores the original pushes the rest onto your app. You build the resize pipeline. You set the cache headers by hand. You hope nothing regresses when traffic spikes. A CMS that treats media as delivery does that work once, for every asset.

Storage and delivery are not the same job. Storage keeps the bytes safe. Delivery needs the right format, the right size, and a cache policy. Conflating the two is why image-heavy pages stay slow on fast hosting.

This is why "just put it in a bucket" is an incomplete answer. Raw object storage has no image variants, no processing step, and no default cache policy. It will serve a 6MB original to a phone on a slow connection, exactly as it serves everyone else. Nothing in that setup tells a media asset apart from any other blob.

A CDN in front of the bucket helps with geography. But it can only cache what the origin tells it to cache. With no Cache-Control header, a CDN has no signal for how long an image is safe to keep at the edge. The header and the CDN are two halves of one mechanism.

How Draftbase handles media management and CDN delivery

Media management in Draftbase is a pipeline, not a single upload endpoint. Four steps, in order: upload, store, transform, serve. Each one exists to remove a specific failure mode teams hit when they build this themselves, and entry-level versioning runs across all four.

This mirrors the write/read split Draftbase already uses for content. Writes go through an authenticated management surface. Reads go through a delivery path built for speed. Upload and confirm are management operations. The CDN-fronted URL is delivery: cached and public.

Need to draft alt text for an asset before it's uploaded? Try the free alt text generator.

1. Upload — presigned, confirmed separately

The API issues a presigned upload URL and form fields for the file. The browser posts the file directly to storage from there. A separate confirm call then registers the asset — the file bytes never route through the API server. The asset starts life with a pending status until processing finishes.

2. Store — one immutable key per file

The original lands under a timestamp-prefixed key, scoped to the org and the environment. No object is ever overwritten: replacing an asset repoints it at a new key and keeps the asset ID, so entries referencing it stay intact and the URL changes with the file.

3. Transform — WebP on a dedicated worker

A separate background worker picks up the original once it lands in storage. It resizes it to fit within 1920x1920 and re-encodes it as WebP at quality 82, off the request path. An upload never blocks a request/response cycle on the main API while this runs, even if the resize takes several seconds on a large original.

4. Serve — immutable for a year, via CDN

Objects carry Cache-Control: public, max-age=31536000, immutable. Public lets any CDN or shared cache store the response, not only the requesting browser, and immutable tells both to stop revalidating. Correct precisely because step 2 never mutates a stored object.

Together these cover the four places a homegrown pipeline usually breaks. A blocked API request during upload. An unprocessed original served at full size. A missing cache header that forces a re-fetch every load. A swapped image with no record of when it changed. Media is referenced from entries as a field type, so that last one is covered by the same revision history every other field uses — swapping an image shows up in the entry's history and rolls back the same way.

What a delivery URL actually looks like

An asset's url is the CDN host joined to its storage key. Nothing else. Both the original and the processed variant follow the same shape:

# what the browser uploaded, before the worker runs
https://cdn.example.com/<orgId>/production/originals/1754870400000-hero.jpg

# what the asset points at once processing finishes
https://cdn.example.com/<orgId>/production/processed/1754870400000-hero.webp

No query parameters. No ?w=800&format=webp to append, because there is nothing to ask for at request time — the transform already happened, once, at upload. That is a real architectural difference from an imgix-style CDN, and worth being plain about: you cannot generate an arbitrary crop from the delivery URL, and you do not pay a transform per unique variant either. One canonical file per asset, cached immutably at the edge, with a URL safe to hardcode in an <img> tag.

None of it needs configuration before an upload works. The presigned URL flow, the processing trigger, and the cache headers are wired up by default in every environment. The knobs that are yours are the org's resize settings — on/off, max width, max height — and its upload size limit, which defaults to 25MB.

How much does WebP conversion save on storage and load time?

WebP files are typically 25-34% smaller than a comparable JPEG, and 26% smaller than a comparable PNG, per Google's own published figures. (Source) Draftbase re-encodes every processed upload to WebP at quality 82. The saving lands on the stored asset itself, so it applies before any delivery variant exists.

Source formatWebP equivalentSize reduction
JPEGWebP, lossy, comparable quality25-34% smaller
PNGWebP, lossless26% smaller
PNG with transparencyWebP, lossless, alpha preserved22% additional bytes for the alpha channel

All three figures are Google's own published measurements for the format. (Source) They describe the encode, not our pipeline — what Draftbase adds is that the encode happens on every upload with nothing to configure.

That one conversion pays twice. Storage is smaller, so a plan quota stretches further and the media library costs less to keep. Transfer is smaller too, so every visitor downloads fewer bytes for the same image on the same page.

The resize step compounds it. Uploads are fitted inside 1920x1920 before encoding, and a smaller original is never enlarged to match. A 6MB camera photo becomes a web-sized WebP once, at upload, instead of being shipped at full resolution to every mobile visitor forever.

Fewer bytes on the wire is the most direct lever on Largest Contentful Paint. The LCP element on a content page is usually an image. Every current major browser supports WebP, so the smaller format costs you no reach.

Can you turn image resizing off?

Yes. Resizing is an org-level setting with three knobs: on/off, max width, and max height. Raise the max dimensions and larger images survive at full resolution. Turn resizing off and the worker stores your original untouched. Everything else still runs. The asset becomes ready, gets CDN cache headers, and versions with its entry.

WebP is the standard, not the optional part. Resizing to fit 1920x1920 and encoding at quality 82 is what every org gets from the first upload, with no setup step. The knobs exist for the images that don't fit that default, not because the default is a guess.

The reasons to turn resizing off are narrow and worth naming. Large artwork or photography that has to keep its full pixel dimensions. Assets that must stay bit-identical to what was uploaded, for legal or archival reasons. Source files handed to a downstream tool that needs the original format. For most of those, raise the max width and height instead. You keep the WebP saving and give up only the downscale.

How a CMS and a CDN divide the work

The split is cleaner than most integrations make it look. The CMS owns the asset record: which file exists, its dimensions and size, its alt text and tags, which entries reference it, and which processed variant is canonical. The CDN owns the bytes in flight: the edge cache, the geographic distribution, and the TLS termination near the visitor. Neither half is useful alone. A CDN with no record of what an asset is cannot tell a canonical variant from an abandoned original, and a headless CMS with no edge serves every visitor from one region.

URL rewriting is where the seam usually shows. In an integrated setup there is nothing to rewrite: the asset record stores a storage key, and the API returns the CDN host joined to that key, so the URL a frontend renders is already the edge URL. In a bolted-on setup — the WordPress CDN plugin pattern — the CMS emits its own origin URLs and something has to find and rewrite them afterwards, in a filter, a template, or worse, a regex over rendered HTML. That plugin is not a feature. It is a symptom of the CMS never having owned delivery.

Three failures follow from that seam, and all three are common. A stale cache after a replace, because the CDN keys on a URL that did not change when the file did. Broken invalidation, because the CMS has no way to tell the CDN which paths to purge, so someone purges everything and absorbs the origin traffic. And orphaned originals, because deleting an entry deletes a record while an unreferenced file sits in storage forever, paid for monthly.

Draftbase closes the first by never reusing a key: a replace repoints the asset at a new one, so the URL changes and the CDN fetches fresh with no purge involved. That is also why the cache header can be immutable. The third is handled at delete time — an asset still referenced by an entry returns a conflict listing the entries that reference it, rather than leaving a broken image behind.

Media management and CDN delivery approaches compared

Three common setups: your own upload path against a plain bucket, a generic headless CMS media field, and Draftbase's pipeline. The differences concentrate in processing and caching. Those are the two steps teams skip first under deadline pressure. None of the three is wrong on day one. The gap widens with asset count and traffic, which is when a manual pipeline is hardest to retrofit.

ApproachSelf-hosted uploads (app server / plain storage bucket)Generic headless CMS media fieldDraftbase media
Upload pathBlocks the app serverPresigned upload, varies by vendorPresigned upload, confirmed separately from the write path
ProcessingCustom pipeline you buildVendor-dependentDedicated worker, decoupled from the API
Output formatWhatever was uploaded, at full sizeVendor-dependentWebP at quality 82, resized to fit 1920x1920
CachingManual header configVendor defaultCache-Control: public, max-age=31536000, immutable on every object
VersioningManualVendor-dependentTied to entry revisions like any other field

Upload path and processing get the attention, because they're visible during development. Caching and versioning are the rows teams regret skipping months later. Both work fine in a demo. At scale one costs money in origin bandwidth, the other costs trust in an unexplained image swap.

Media management pitfalls

The most common mistake is serving the original upload instead of a processed variant. A camera photo can run several megabytes. Shipping that to a browser wastes bandwidth on every request. It also slows Largest Contentful Paint, the exact metric a CDN is meant to help. Resize and compress once at upload, then serve the smaller result. One cost at upload beats a repeated cost on every page view.

The second mistake is skipping cache headers. Every request re-fetches from origin, even when the file hasn't changed. That adds visitor latency and origin load for no benefit. An object whose URL is unique to its contents can carry a long max-age with immutable, which stops browsers and edges revalidating something that will never differ.

The third mistake is treating media as separate from content versioning. A swapped image silently changes what a published entry shows. No audit trail, no way to answer what it looked like last week. All three trace to one root cause. Media gets handled as a file operation instead of as content that's processed, cached, and versioned.

The fourth is caching a URL that can change underneath you. A long max-age on a key that gets overwritten on replace means an updated image never reaches anyone until the cache expires, and cache-busting query strings only shift the problem to whoever forgets to append one. Give every file its own key and the problem disappears: a new file is a new URL, and the old one can be cached forever without lying.

The fifth is uploading through the app server rather than presigning direct-to-storage. A 20MB file on a slow connection holds a request thread open for the whole transfer, and enough concurrent uploads starve the API for everyone else in the org. The bytes have no business touching your application at all.

The sixth is having no maximum-dimension policy, and it is the one we learned the hard way. Image libraries have their own limits that do not match what a product accepts: sharp refuses to decode past roughly 0.27 gigapixels by default, which a stitched panorama trips, and a processing job with a fixed memory and time budget can run out of both on a large enough original before it ever reaches the encode step. Decide the ceiling deliberately — in Draftbase that is the org's resize setting — rather than discovering it as a failed asset. Client-side re-encoding deserves the same suspicion: a picker that decodes to a canvas and re-encodes before upload can hand the server a file several times larger than the one the user chose.

None of these are exotic failures. They're the default outcome of wiring uploads to a bucket without also deciding what happens after the upload. Who resizes the file, who sets the headers, what the size ceiling is, and who notices when the file behind an entry changes. A pipeline makes those decisions once, up front, instead of leaving them to whoever hits the bug first. If what you actually need is per-request transforms from an existing origin, that is a different tool — our image CDN guide covers when one is worth adding.

Try media management on Draftbase

Upload an image and pull it from the CDN with cache headers already set. You upload, the pipeline handles the rest.

Hobby is free, no card. Startup is $49/mo when you outgrow it. The price is on the pricing page, where prices go.

No migration quarter, no kickoff workshop. Define a template and ship something today.

Frequently asked questions

Do media uploads go through Draftbase's main API?

Only the request and confirmation steps do. The API issues a presigned upload URL, then the browser uploads the file bytes directly to storage. The API then confirms the asset, registering it as pending. The file itself never passes through the API server, so a large upload can never tie up a request thread there. This is the same reason a slow upload on a bad connection doesn't degrade the API for every other user in the org at the same time.

What processes an uploaded image?

A dedicated background worker, separate from the main API, resizes and optimizes the image once it lands in storage. This keeps a large upload from tying up an API request/response cycle while processing runs. The worker writes the final processed file back to storage and updates the asset's status once it finishes, so the API never has to wait on that work synchronously.

Does Draftbase convert uploaded images to WebP?

Yes. Every processed upload is re-encoded to WebP at quality 82 and resized to fit within 1920x1920 by default, without ever enlarging a smaller original. Google reports WebP lossy files run 25-34% smaller than comparable JPEGs and WebP lossless files run 26% smaller than PNGs (Source: developers.google.com/speed/webp), so the stored asset is smaller than the file that was uploaded. Smaller bytes mean less storage against your plan quota and less to transfer on every view, which is where a visitor actually feels it.

Can you disable image resizing in Draftbase?

Yes. Resizing is an org-level setting with three values: on/off, max width, and max height. With it off, the worker stores your original file untouched, at its original dimensions and in its original format. WebP conversion is the standard path, though. It applies to every upload unless resizing is switched off. The rest of the pipeline is unaffected either way: the asset still becomes ready, still ships with CDN cache headers, and is still versioned with the entry that references it. For oversized artwork, raising the max width and height is usually better than turning resizing off, since it keeps the WebP saving and only drops the downscale.

What cache headers does Draftbase set on media delivery?

Every stored object carries Cache-Control: public, max-age=31536000, immutable — one year, cacheable by any CDN or shared cache, and never revalidated. That is safe because a stored object is never mutated: keys are timestamp-prefixed, and replacing an asset repoints it at a new key rather than overwriting the old one. So the URL changes when the file changes, which is what makes an immutable year-long cache correct instead of reckless.

How does a CDN work with a headless CMS?

They own different halves of one path. The CMS owns the asset record: which file an entry points at, its dimensions, its alt text, and which processed variant is canonical. The CDN owns the edge cache and the geographic distribution of the bytes. In Draftbase the delivery URL is the CDN host plus the asset's storage key, so there is no plugin rewriting URLs and no second system to keep in sync — the CMS emits the CDN URL directly, with cache headers already set on the object. A bolted-on CDN, the WordPress plugin pattern, is what happens when the CMS does not own delivery and something has to rewrite its URLs after the fact.

Should I use a separate image CDN with my CMS?

Usually not, if your CMS already processes and serves media through a CDN. A separate image CDN like imgix or Cloudinary earns its place when you need per-request transforms — arbitrary crops, art direction, format negotiation per device — from a source of truth your CMS does not control. Draftbase processes once at upload instead: one WebP variant, sized to fit your org's maximum, served immutably. If you need on-the-fly variants of the same original, an image CDN in front of the delivery URL still works. Our image CDN guide covers how they differ and what to check before adding one.

Is a media asset versioned like other content?

Yes. Media is referenced from entries as a field type, the same as text or reference fields, so swapping an image is tracked in that entry's revision history. There's no separate media-versioning system to learn — an image field behaves the same as any other field when an entry gets a new revision, which means rollback works the same way too.

What happens if image processing fails?

The asset is marked failed with the error message attached, instead of silently staying in a pending state. You can see the failure and re-upload rather than debugging a missing image later. That status is visible on the asset record itself, so a failed process step surfaces as data you can query, not a gap you only notice when a page renders a broken image.

What triggers Draftbase's image size validation message?

Two separate limits, checked at different points. Upload size is enforced at the storage layer: each org has a configurable maxUploadBytes, and the presigned upload rejects anything over it with a 413 Payload Too Large before the file even reaches processing. Pixel dimensions are checked during processing, since decoding an oversized image can exhaust memory regardless of file size — that limit surfaces as a failed asset with the error attached, visible on the asset record.

Related reading

Go deeper on Media & CDN