
Images make up 50–80% of the average page's weight. Largest Contentful Paint, the timer Google starts when your page opens and stops when the biggest thing on screen finishes drawing, is almost always timing a photo. 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. One caveat for the record before any numbers show up: the Directus stack ran on a laptop with no network hop, while Sanity was reached over the real internet. So the fair comparison is the ratio between cold and warm requests, not the raw milliseconds. We'll flag that again wherever it matters.
Note for anyone editing this post: every Directus number below comes from a single-machine, localhost Docker setup with no real network hop, no concurrent traffic, and no distance between services. A production Directus deployment (real cloud VM, real S3/R2 endpoint, real internet path to visitors) will be slower than every Directus figure quoted here. Do not present these numbers as "what Directus delivers in production." Present them as "what the mechanism is capable of on a local rig, relative to Sanity's real hosted result." Where a number needs restating for a client or on social media, add a line like "measured on local infrastructure, production latency will be higher depending on your hosting and region."
The Difference Behind Every Number
Sanity is 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 is 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.
That split creates three contenders, not two:
-
Sanity: fully managed, nothing to run.
-
Directus Cloud: Directus with the delivery parts pre-assembled for you.
-
Self-hosted Directus: every part is yours to choose, pay for, and keep running.

What We Had to Set Up
Sanity needed one part: a sign-up form, then upload.
Self-hosted Directus needed four supporting services before a single photo could move:
-
Database: the filing cabinet holding names, sizes, and who uploaded what (postgis/postgis:16-master)
-
File storage: the warehouse where the actual photos sit (minio/minio:latest, the real S3 API, not a mock)
-
Cache: short-term memory so the same work isn't redone (redis:7-alpine, backing CACHE_STORE=redis)
-
CDN: a pickup point near your visitor instead of shipping every photo from one factory (nginx:alpine with a custom proxy_cache, standing in for Cloudflare/Fastly, exposing X-Cache-Status: HIT/MISS)
Directus 12.3.1 was confirmed live from its own /server/info endpoint. Every number below is the output of a script, not a hand-picked screenshot, so it's reproducible by anyone on the team.
[IMAGE PLACEHOLDER: Five-container setup diagram]
Six Lines Is the Whole Story
Before we get into 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. Keep this section as a reference and skip ahead if you just want the results.
Resumable upload (this sets up Round 1):
TUS_ENABLED: true
TUS_CHUNK_SIZE: 10mb
This turns on chunked, resumable uploads. 10MB is S3's own multipart-upload floor, not an arbitrary choice.
Storage (this sets up Round 2):
STORAGE_MINIO_DRIVER: s3
STORAGE_MINIO_ENDPOINT: <http://minio:9000>
STORAGE_MINIO_FORCE_PATH_STYLE: true
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.
Transform limits (this sets up Round 3):
ASSETS_TRANSFORM_MAX_CONCURRENT: 4
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION: 6000
These are anti-abuse limits on image resizing. Sanity doesn't need an equivalent, because you don't control its infrastructure and can't overload it.
Caching (this sets up Round 5):
CACHE_STORE: redis
ASSETS_CACHE_TTL: 30d
proxy_cache_valid 200 301 302 30d;
The first two lines tell Directus to use Redis. The third line is a separate setting on the Nginx CDN layer in front of it. Redis speeds up Directus's own internal work; Nginx's proxy_cache is what actually stores the finished image bytes at the edge.
Round 1: What Happens When an Upload Drops Halfway?
We uploaded a 3.17MB JPEG. Directus finished in 429ms. Sanity took 38.6 seconds.
That gap isn't really "Sanity is slow." Sanity's upload pipeline does more work per file (dimension extraction, placeholder generation), and the request crosses the real internet while Directus was local. The more telling test: we uploaded a larger 15.92MB file and killed the connection on purpose, partway through, then asked Directus where it stopped.
It 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.
(Local-test 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.)
[IMAGE PLACEHOLDER: Round 1 resumable upload visual]
Round 2: Who Owns the Warehouse?
No stopwatch in this round. It's a design choice, and it's a knockout.
Directus ships 6 official storage drivers: local, S3, GCS, Azure Blob, Cloudinary, and Supabase. Because the S3 driver speaks the standard S3 protocol, it also covers Cloudflare R2, MinIO, DigitalOcean Spaces, and Backblaze B2.
Sanity offers 1: its own. Assets are reachable only through cdn.sanity.io, backed by Sanity's Content Lake.
Your photo library either lives somewhere you can move out of, or it doesn't.
[IMAGE PLACEHOLDER: Storage backend comparison]
Round 3: Can It Use the Newest Photo Format?
Both platforms shrink photos on demand: one photo goes in, the right size comes out. We measured the same 1200px transform on both:
|
Format
|
Directus
|
Sanity
|
|
JPEG
|
125.6 KB
|
114.7 KB
|
|
WebP
|
93.3 KB
|
75.3 KB
|
|
AVIF
|
277.5 KB
|
HTTP 400
|
Ask Sanity for AVIF, the newest format and typically 10–20% smaller than WebP for the same picture, and it returns an outright error: "Parameter 'fm' must be one of: 'jpg', 'pjpg', 'png', 'webp', 'gif' or 'json'." Sanity's automatic format choice stops one generation back, at WebP. Directus produced the AVIF.
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
|
Reminder: Directus is local, Sanity is over the internet. Read the cold-to-warm ratio (roughly 11× for Directus, roughly 16× for Sanity), not the absolute gap.
(Local-test 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 the cold and warm numbers. The ratio between them is what should carry over, not the millisecond values themselves.)
[IMAGE PLACEHOLDER: Format & transform speed chart]
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.)
[IMAGE PLACEHOLDER: Stale cache incident timeline]
Round 6: What About Visitors on the Other Side of the World?
This one is physics, not software, and we're flagging it clearly: it's not a live geographic test. The bench ran a single Nginx edge box, not 100+ worldwide points of presence. The figure below is drawn from the article's estimate, not a direct measurement.
A far-away visitor with no nearby pickup point pays an estimated +150–300ms of extra round-trip per image, multiplied across every image on the page. Sanity ships with edge points worldwide by default, no setup required. 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. Treat these as a controlled comparison of mechanisms (does it resume, does it cache, does it support AVIF), not as a promise of production latency. 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
|
HTTP 400 (unsupported)
|
Metadata & caching
|
Metric
|
Directus
|
Sanity
|
|
LQIP
|
none
|
491-char base64
|
|
BlurHash
|
none
|
Generated
|
|
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
|
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
|
|
|
🏆
|
Directus wins capability and ownership. Sanity wins everything about delivering the photo to a real visitor with nobody watching. Directus Cloud sits in between, taking the same capability wins without the infrastructure work.
[IMAGE PLACEHOLDER: Scorecard summary graphic]
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.
