
Images are 50–80% of what a page weighs, and the picture is usually what Google's speed score is timing. So the CMS you pick isn't just a content editor. It's a photo delivery service you're hiring.
We didn't want to just repeat vendor claims, so we ran both pipelines side by side: a real self-hosted Directus 12.3.1 stack in Docker, and a real Sanity project on the free plan, same test script, same day.
Note: the Directus stack ran on a laptop with no network hop, while Sanity was reached over the real internet. Compare the ratios between cold and warm requests, not the raw milliseconds. We'll repeat this note wherever it matters.
The Difference Behind Every Number
Sanity: rent a finished pipeline. A delivery service you subscribe to. You hand over the photo; it arrives at the visitor. Storage, resizing, and delivery are all decided for you, and out of your hands.
Directus: get the parts, build your own. A toolkit you assemble yourself. Every part is swappable, and every part is yours to run. That's a higher ceiling on speed, reached only after the assembly work is done.
Three Contenders, Not Two
-
Sanity: fully managed. Nothing to run. (Browser → Global Edge CDN → Image Pipeline → Content Lake)
-
Directus Cloud: Directus with the delivery parts pre-assembled for you. (Browser → AWS CDN → Directus App → Managed Storage on AWS S3)
-
Self-hosted Directus: every part is yours to choose, pay for, and keep running. (Browser → Cloudflare, optional → Directus Server → Object Storage on S3/R2/GCS)

What We Had to Set Up
Sanity, one part: a sign-up form. Then upload.
Directus, four parts to run:
-
Database: the filing cabinet holding names, sizes, and who uploaded what
-
File storage: the warehouse where the actual photos sit
-
Cache: short-term memory, so the same work isn't redone
-
CDN: a pickup point near your visitor, instead of shipping every photo from one factory
Note: the Directus stack ran on a laptop with no network hop, while Sanity was reached over the real internet. Compare the ratios, not the raw milliseconds.
Not Simulated: Here's Exactly What Ran
Self-hosted Directus 12.3.1, five containers:

Against: a real Sanity project on the free plan, same test script, same network, same day.
Every round below is the output of a script (bun run all), not a hand-picked screenshot. Anyone on the team can re-run it and get the same shape of result.
Six Lines Is the Whole Story
Before the six rounds below, here's a preview: the entire architectural argument of this article comes down to a handful of config lines in Directus. Each one is a setting we had to turn on ourselves; Sanity needs none of them.
Round 1, Resumable upload:

This turns on chunked, resumable uploads. 10MB is S3's own multipart-upload floor, not an arbitrary choice.
Round 2, Storage:

This points Directus at an S3-compatible bucket. Swap the endpoint for https://<id>.r2.cloudflarestorage.com and this is production Cloudflare R2. Nothing else changes.
Round 3, Transform limits:
These are anti-abuse limits on image resizing. Sanity doesn't need an equivalent, because you don't control its infrastructure.
Round 5, Caching:

Redis backs the headers; Nginx's proxy_cache is what actually stores the bytes at the edge.
Round 1: What Happens When an Upload Drops Halfway?
We killed the connection partway through a 15.92MB upload. Directus resumed at exactly 10.00MB, not zero, and re-sent nothing.
Nobody on your team has to re-upload a big file because the wifi blinked. Sanity's upload service has no equivalent mechanism.
Source: Directus 429ms, Sanity 38.6s (upload). TUS resume: offset 10.00MB of 15.92MB after kill, 0 bytes re-sent.
Note: 429ms was measured on localhost with no network in the path. A production Directus instance, reached over the internet with a real S3/R2 upload target, will take longer than 429ms. The point this round makes is the resume behavior, not the raw upload speed.
Round 2: Who Owns the Warehouse?
6 official storage options for Directus, plus anything that speaks the same standard.
1 for Sanity: its own. Assets are reachable only through Sanity.
Your photo library either lives somewhere you can move out of, or it doesn't. It's a design choice.
Source: Directus drivers, local, S3, GCS, Azure Blob, Cloudinary, Supabase; the S3 driver also covers R2, MinIO, Spaces, B2. Sanity: Content Lake only, served via cdn.sanity.io.
Round 3: Can It Use the Newest Photo Format? Both Can.
Our script asked for fm=avif and got an error, the wrong way to ask. Sanity has produced AVIF since August 2024. The difference is when: Directus makes it on request, every time. Sanity hands the first visitor WebP, builds the AVIF in the background, then serves it from then on.
AVIF is 10–20% smaller than WebP for the same picture. Neither platform leaves you a format behind, correcting our own earlier read of this test.
On latency, both platforms cache their own work after the first render:
| |
Directus
|
Sanity
|
|
Cold (first request)
|
211.6ms
|
1161.5ms
|
|
Warm (median of 5)
|
18.6ms
|
74.0ms
|
Note: 211.6ms and 18.6ms are localhost figures with zero network latency. On a production Directus deployment, add real network round-trip time and server load on top of both numbers. The ratio between cold and warm is what should carry over, not the millisecond values themselves.
Source: cold 211.6ms → warm 18.6ms (Directus); cold 1161.5ms → warm 74.0ms (Sanity). JPEG 125.6KB/114.7KB, WebP 93.3KB/75.3KB. Directus AVIF 277.5KB on demand. Sanity fm=avif → HTTP 400 by design; AVIF ships via auto=format + Accept: image/avif (Sanity docs, Aug 2024).
Round 4: What Does a Visitor See While the Photo Loads?
This is the most visible round on an actual page.
Querying Directus's file metadata (/files/:id) returns an empty metadata object, not sparse, empty. Querying Sanity via GROQ returns four extras generated automatically at upload: a 491-character base64 LQIP, a BlurHash string, a ThumbHash, and a dominant palette color (#7c6c94). Both platforms agreed on the raw dimensions (4000×3000), so this isn't a capability gap in reading the file, only in what's generated at upload time.
On a slow connection, that difference is visible: Directus shows a grey box while the real image downloads. Sanity shows a blurred preview that sharpens into place, so the page feels finished sooner and nothing jumps around as photos arrive.

Round 5: We Changed the Photo. Visitors Keep Seeing the Old One.
We replaced a file's contents in Directus without changing its URL, then measured three layers:
| |
Latency
|
|
Origin (median)
|
17.9ms
|
|
Edge, first request (MISS)
|
26.4ms
|
|
Edge, repeat (HIT, median)
|
0.5ms, 33× faster than origin
|
That edge cache is genuinely valuable until content changes. After swapping the file's bytes at the same URL, the origin server served the new photo immediately. The edge kept handing out the old photo, reporting x-cache-status: HIT on stale content, until an engineer manually ran rm -rf /var/cache/nginx/assets/* && nginx -s reload.
Sanity cannot reproduce this failure mode. A changed photo gets a new content-hash URL, so there's nothing stale to clear. That's the same property that makes its cache-forever model safe.
(Local-test note: 17.9ms / 26.4ms / 0.5ms were all measured between containers on one machine, with no real client, no real distance, and no real load. In production, expect higher absolute numbers on all three rows. The finding that matters is the stale-cache behavior itself, not the specific millisecond figures.)
Round 6: What About Visitors on the Other Side of the World?
+150–300ms of extra waiting for a far-away visitor with no pickup point nearby, multiplied across every image on the page.
Sanity ships with pickup points worldwide, no setup. Directus can match that speed, but only after someone builds and maintains the equivalent.

For the Record: The Raw Numbers
Every Directus figure in both tables below was measured on a local, single-machine Docker setup, not a production deployment. A live Directus instance on real hosting, with real network distance to visitors, will show higher numbers than what's listed here.
Upload & transform
|
Metric
|
Directus
|
Sanity
|
|
Upload time (3.17MB)
|
429ms
|
38.6s
|
|
Transform cold
|
211.6ms
|
1161.5ms
|
|
Transform warm (median of 5)
|
18.6ms
|
74.0ms
|
|
JPEG output (1200px)
|
125.6 KB
|
114.7 KB
|
|
WebP output (1200px)
|
93.3 KB
|
75.3 KB
|
|
AVIF output (1200px)
|
277.5 KB (on demand)
|
not measured, async via auto-format
|
The blank AVIF cell for Sanity deserves an explanation rather than reading as a loss. Asking for fm=avif directly returns an error, but that's Sanity's documented behavior for that parameter, not a missing feature: it never claimed to support AVIF that way. Sanity encodes AVIF asynchronously and serves it through auto=format instead, and since our script never sent that parameter, this row is genuinely unmeasured rather than failed.
Metadata & caching
|
Metric
|
Directus
|
Sanity
|
|
LQIP
|
none
|
491-char base64
|
|
BlurHash
|
none
|
VAE{S3{xRV3EIK[AtdOFM,Ffa7Ork9wIou74M,w[%E$e
|
|
Dominant color
|
none
|
#7c6c94
|
|
Origin latency (median)
|
17.9ms
|
n/a
|
|
Edge MISS
|
26.4ms
|
n/a
|
|
Edge HIT (median)
|
0.5ms (33× origin)
|
n/a
|
|
Stale after content swap?
|
Yes, until manual purge
|
N/A: new content hash means a new URL
|
Cache headers were read directly off the HTTP response, not summarized. HIT was observed on stale content until rm -rf /var/cache/nginx/assets/* && nginx -s reload was run by hand.
The Scorecard
|
Category
|
Self-hosted Directus
|
Directus Cloud
|
Sanity
|
|
Uploading a photo
|
🏆
|
🏆
|
|
|
Where the photos live
|
🏆
|
|
|
|
Resizing and reformatting
|
🏆
|
🏆
|
🏆
|
|
Preview while loading
|
|
|
🏆
|
|
Replacing a photo
|
|
|
🏆
|
|
Speed for far-away visitors
|
|
|
🏆
|
|
Nothing to run yourself
|
|
|
🏆
|
Resizing and reformatting is a draw: both platforms produce AVIF, Directus on request, Sanity in the background.
Verdict
Sanity sells you a fast pipeline. Directus sells you the parts to build an even faster one.
-
Fastest by default: Sanity. Sign up and it's already quick everywhere.
-
Highest ceiling, cheapest at scale: self-hosted Directus, once it's built.
-
The middle path: Directus Cloud, with delivery included.
