Skip to content
scroll to zoom · drag to pan

How this compares

Three different kinds of number appear below, and they do not deserve the same trust:

  • Our numbers are measured, and the measurement is reproducible. The transform figures come from the transform benchmark, which runs against the public API with an ordinary runtime key and writes its own JSON; what it does, step by step, is at the bottom of that page, so you can re-run it on your own tenant.
  • Their numbers are quotes from their own documentation, with the date we read it. Not from memory, not from a third-party comparison post, not from a blog. Every competitor row is stamped with the URL it came from and the day it was read.
  • Rows we could not confirm say no documentado. Not “no”, not a blank cell — a blank cell reads as absence and absence is a claim. Where a vendor’s documentation is silent, this page is silent too, and the footnote says what was tried.

That is the entire credibility of the page, so it goes first rather than in a footer. A comparison table nobody can audit is worse than no table.

Before any table, because this is the part a vendor’s own comparison page never writes.

We do not own a PoP network. Delivery rides Cloudflare. If your requirement is a contractual edge footprint, or a specific set of points of presence, you are buying our opinion of someone else’s network. Bunny publishes 119 PoPs on its standard network and 10 on its volume network.

There is no SLA and the team is small. No published uptime commitment, no support tier, no procurement paperwork. If an availability number has to appear in a contract, this is not the product.

No DAM UI. There is no asset browser for a non-technical user to search, tag and organise in. ImageKit sells DAM storage as a metered line on every plan, which tells you it is a real product surface there and not here.

No AI auto-tagging, moderation or background removal in the ingest path. Cloudinary wires auto-tagging, moderation (WebPurify, AWS Rekognition, Google AI Video Moderation, Perception Point), object detection and background removal into the same upload flow. Each of those would be a separate integration for you here.

No DRM. Bunny Stream documents enterprise-grade multi-DRM. We have signed URLs, which is a different and weaker thing: a signed URL controls who can fetch the bytes, not what the player may do with them afterwards.

No official MCP server. This matters because we claim to be agent-first, and on this specific axis three competitors are ahead of us:

  • Cloudinary ships four first-party MCP servers (Asset Management, Environment Config, Structured Metadata, Analysis) with OAuth on the remote ones, plus llms.txt and a cloudinary_transformation_rules.md written to keep a model from hallucinating transform syntax.
  • Cloudflare runs “a catalog of managed remote MCP servers which you can connect to using OAuth”, and its API server exposes “over 2,500 endpoints … through just two tools: search() and execute()”.
  • ImageKit’s “API MCP server lets an AI assistant manage your ImageKit media library on your behalf … covers all of ImageKit’s public APIs”, installed together with an agent skill bundle via npx skills add imagekit-developer/skills --all.

What we have instead is a typed SDK, llms.txt, a .md route on every page of this site and the agent files. That is a real surface and it is not an MCP server.

We require an ingest step; four of the six do not. imgix connects to an existing S3, GCS, Azure, DigitalOcean, Cloudflare R2, Wasabi or Linode bucket as a Source — “Connects to an existing Amazon S3 bucket with its own credentials” — and never takes ownership of your storage. ImageKit does both, trying its Media Library first and falling back to an attached external origin. Vercel proxies only. Bunny is proxy-first. If “we cannot re-upload 40 TB” is your constraint, that alone decides it.

The architecture row: does it store your originals?

Section titled “The architecture row: does it store your originals?”

This is the row that actually separates these products, and most comparison tables never draw it. Everything downstream — whether you can add a variant later, whether there is a storage bill, whether “dedup” can even be defined — falls out of this answer.

Product Stores your originals? The quote
Cloudinary Both. Storage is the primary model fetch is the proxy mode, but “Fetched assets are cached in your Cloudinary product environment for performance reasons, and this storage counts against your quota”
Cloudflare Images (stored) Yes “Images Stored and Images Delivered apply only to images that are stored in your Images bucket”
Cloudflare transformations (your origin) No “If you optimize an image stored outside of Images, then you will be billed only for Images Transformed”
Bunny Optimizer Proxy first — Bunny Storage is a separate product with its own price “Resize, crop, and modify images on the fly with simple url query parameters” … “without storing multiple file versions”
imgix No — proxy only “The first step in working with Imgix is to create a Source, which connects Imgix to your asset storage”
ImageKit Both “ImageKit always tries to fetch the file from the integrated Media Library first. If the file is not found, it tries to fetch it from the attached external storage (origins) sequentially”
Vercel No — stores nothing but the output “Only the optimized output is stored, and the stored image is served like any other blob”
nitida Yes — content-addressed by SHA-256 The SHA is the asset; see what this is

Cloudflare is two products and one row would lie about both. The three meters — Images Transformed, Images Stored, Images Delivered — do not all apply at once: transforming an image on your own origin bills only the first. Splitting the row is not pedantry; it is a 3× difference in what you pay.

Pricing shapes — and why there is no single dollar column

Section titled “Pricing shapes — and why there is no single dollar column”

These six bill in six incompatible units. Credits, per-image, per-minute, per-GB, per-website, and per-transformation-plus-cache-read-plus-cache-write. A normalised “$ per month” column would require inventing a traffic mix, a hit rate, an asset count and a variant ladder, and then presenting the arithmetic on those invented inputs as a fact about the vendor. That is fabrication with a table around it, so this page does not do it. Each row gets its own unit and its own quote; the conversion to your bill is yours to do, on your numbers.

Product Billing unit Quoted rate Free tier
Cloudinary Credits, fungible across three things “1 credit = 1,000 transformations image and video processing OR 1GB managed storage for all digital assets OR 1GB video bandwidth high-performance CDN”. Plus “$99 Per month … 225 monthly credits” “$0 Free forever … 25 monthly credits”
Cloudflare Images Per image, three meters Transformed: “First 5,000 unique transformations included + $0.50 / 1,000” · Stored: “$5 / 100,000 images stored / month” · Delivered: “$1 / 100,000 images delivered / month” “up to 5,000 unique transformations each month for free”. Stored and Delivered have no free tier
Cloudflare Stream Per minute “$5 per 1,000 minutes stored” with “no additional egress (traffic/bandwidth) fees” · “$1 per 1,000 minutes delivered” · “Ingress … and encoding are always free” no documentado
Bunny Per website, flat — plus per-GB bandwidth Optimizer “$9.50/website”, “Unlimited transformations” — but “CDN bandwidth costs are applied separately”: $0.01/GB EU+NA on the standard network, from $0.005/GB on the volume network. Storage $0.01/GB (HDD, one region) None permanent. “14-day free trial”, “No credit card required”
imgix Credits, per GB “Credits are a measure of capacity that are consumed by media management, delivery, and transformation” — management “2 credits / GB / month”, delivery “1 credit/GB”, transformation “Varies by feature”. “$25/mo” Starter (100 credits) → “$500/mo” Growth Plus. Overage $0.25 → $0.16 per credit None permanent. “Get 30 days and 100 credits”, after which “they will expire”
ImageKit Per GB delivered + per GB stored + video units + extension units + users + purges Lite “$9/mo”: “40 GB bandwidth” then “$0.5/GB”; “10 GB DAM storage” then “$0.1/GB”. Pro “$89/mo”: 225 GB / $0.45 per GB. Video billed in VPU = seconds × resolution units × codec units (AV1 = 10) Real and perpetual. “Forever Free ($0/mo)”: “20 GB bandwidth”, “3 GB DAM storage”, 500 video units, 2 users
Vercel Three units, none of them per-GB of source “Image transformations 5K/month $0.05 - $0.0812 per 1K” · “Image cache reads 300K/month $0.40 - $0.64 per 1M” · “Image cache writes 100K/month $4.00 - $6.40 per 1M” — plus “charges apply for Fast Data Transfer and Edge Requests” on delivery Hobby: 5K transformations, 300K reads, 100K writes — “Hobby teams are restricted to non-commercial personal use only”
nitida No published price list Onboarding is a conversation; you get an endpoint, a tenant id and keys. See credentials —

Stated explicitly, because each of these is a mistake we would have made from memory:

  1. Cloudflare never bills per GB. Images are billed per image, video per minute. Any “$/GB” figure in a Cloudflare Images column is fabricated, however plausible it looks next to the other rows.
  2. Bunny’s “unlimited” is a billing statement, not a technical ceiling — and it excludes bandwidth. The $9.50/website subscription covers “Unlimited requests · Unlimited optimizations · Unlimited transformations”, and then “CDN bandwidth costs are applied separately”. Quoting the $9.50 alone overstates the saving by however much traffic you serve.
  3. Vercel’s pricing model changed. It is no longer counted in “source images”; it is transformations + cache reads + cache writes. Older knowledge is confidently wrong here. Their legacy pricing page also renders two figures as $5000000.00 — a documentation bug. Those two numbers are not quoted anywhere on this page.

One more counting rule worth reading before you model anything

Section titled “One more counting rule worth reading before you model anything”

Cloudinary does not count one URL as one transformation, in either direction:

“Upload processing: The upload of each image and video asset … counts as one transformation.”

“Since transformations are counted when a new derived asset is generated, multiple requests to the identical transformation URL do not affect transformation counts.”

“Default optimizations count towards your usage, even though the delivery URL does not appear to be different from delivering an original asset.”

Video is priced per second of output: SD 2 · HD 4 · 4K 8 credits per second, and adaptive bitrate with sp_auto runs 8/s to 1080p, 12/s at 1440p, 24/s at 2160p. “Parts of seconds are counted as one second.”

Vercel’s three units also have precise triggers: transformations and cache writes are “billed for every cache MISS and STALE”, while a cache read “is not billed for every cache HIT, only when the image needs to be retrieved from the shared global cache.”

no documentado means the vendor’s own documentation does not state it and we did not find it — see the footnotes for what was searched. It is not a “no”.

Cloudinary Cloudflare Bunny imgix ImageKit Vercel nitida
Video Yes Yes (Stream, billed separately) Yes (Stream) Yes (Video API) Yes Images only 1 Yes
Adaptive bitrate HLS and DASH from one original, sp_auto Multi-bitrate ladder included in the per-minute price Yes; “No Transcoding Fees” HLS/DASH as URL parameters; 6 credits per minute of output HLS and DASH via one sr- parameter; first request answers “202 Accepted” — HLS ladder
Signed URLs Yes, /s--SIGNATURE--/ 2 Yes — stored Images only, and “Images with custom ID paths cannot be made private using signed URL tokens” Yes, at the CDN layer (MD5 basic, SHA256 advanced with geo and rate limits) Yes, MD5, per-Source token; an altered URL returns “403 Forbidden” Yes, with expiry (ik-t, 401 on expiry) 3 no documentado 4 Yes
Content dedup no documentado no documentado no documentado no documentado no documentado Partial: “For local images … the content hash is used”; “Remote images use an absolute url” Yes — SHA-256 is the address
Per-tenant metering Per account: GET /usage returns storage, bandwidth, requests, derived resources. Per API key: no documentado 5 Partial — per-image metadata via the Workers binding. A bytes-and-file-count-by-key endpoint: no documentado 6 Per zone, and it is good: GET /storagezone/{id} returns StorageUsed and FilesStored in one call Per Source, daily CSV, “retained for 90 days” Per account: GET /v1/accounts/usage returns bandwidth_bytes and media_library_storage_bytes. Per-key breakdown: no documentado No — “Vercel tracks events at the team level, counting them across all projects in the team” Per tenant and per key: usage.snapshot() returns storage.totalBytes + storage.assetCount; usage.keys() attributes it
Agent / MCP surface 4 first-party MCP servers, OAuth, llms.txt, .md per doc page, a transformation-rules file for LLMs Managed remote MCP catalog with OAuth; “over 2,500 endpoints … through just two tools” Deliberately no MCP: “No MCP server to configure … The CLI you use is the same interface your agent uses” 7 no documentado 8 Official MCP over the whole public API + an installable skill bundle + llms.txt / llms-full.txt The write-time API is agent-shaped: vercel blob put-image “prints the URL … so scripts and agents can read the result back” Typed SDK, llms.txt, .md on every page, agent files — no MCP server
Hard limits 100 MB before upload_large is required; async above 20 GB. Per-plan maxima: no documentado 9 Remote origin 100 MB / hosted Images 10 MB; 100 MP; 12,000 px (1,200 px for AVIF). Rate limits: no documentado no documentado 10 Canvas 8192×8192; inputs under 100 MB, soft limit 500 MB. Rate limits: no documentado Transform input 20 MB free / 40 MB paid, “adjustable upon request”; video 100 MB / 2 GB; max transform dimension 65535 px (WebP 16383). Reads “near or below 40 requests/second”, then 429 Output max 10 MB; source max 8192 px per side. Source file size: no documentado See variants and presets

Content deduplication is the one row where being sloppy would make this whole page dishonest, because it is the row where nitida looks best.

None of the six documents it. Not one of Cloudinary, Cloudflare, Bunny, imgix, ImageKit or Vercel states that byte-identical uploads collapse to a single stored, billed asset.

That is not the same as saying none of them do it, and the difference is the whole point. “Not documented” is a fact about their documentation; “does not happen” would be a claim about their storage engines, which we have not tested and cannot see. Content addressing is therefore undisputed by their docs as a nitida differentiator — not proven absent in theirs.

What each vendor actually says, so you can judge the gap yourself:

  • Cloudinary — identity is the public_id, not the bytes; unique_filename defaults to true and “appends random characters to the end of the filename to guarantee its uniqueness”. Duplicate detection exists as an opt-in add-on in Beta, and it works by similarity: “images do not need to be identical”.
  • Cloudflare — nothing states that identical bytes collapse. The closest documentation concerns ID reuse. Billing per uploaded image is suggestive, but suggestion is not a source.
  • Bunny — storage is a path-indexed, filesystem-shaped zone, which argues against it. Bunny says neither.
  • imgix — the closest thing is one-origin-many-derivatives, which is not deduplication.
  • ImageKit — duplicates are a search feature, not a storage guarantee: “Any duplicate files will appear at the start of the search results.” Upload naming is by name, not by hash.
  • Vercel — the only documented content addressing among the six, and it is partial: “For local images (/assets/me.png) the content hash is used instead”; “Remote images use an absolute url”. The key is also scoped by Project ID, so there is no dedup across projects, and the same bytes behind two URLs are two entries and two transformations.

On our side the claim is narrow and checkable: the SHA-256 is the address, so the same bytes are the same asset regardless of who uploaded them or what they named the file. The transform benchmark relies on it — it uploads the same photograph twice on purpose and the second upload deduplicates.

Two of these route away from us on purpose. A routing table that always ends in “use ours” is a sales page wearing a table’s clothes.

If this is your constraint Use Why
You cannot re-upload your library — the originals live in S3/GCS/Azure/R2 and must stay there imgix, or Cloudflare transformations over your own origin imgix connects to an existing bucket as a Source and never takes ownership. Cloudflare bills only the transform meter when the original lives outside Images. We require an ingest step; there is no way around it
You are already on Vercel and want next/image to keep working Vercel Zero integration cost by construction — no SDK, no tenant, no second bill. It stores nothing, so there is no storage line at all
You need DRM, or AI auto-tagging and moderation in the upload path Bunny Stream (DRM) · Cloudinary (tagging, moderation, background removal) Both are documented product surfaces there and are absent here
You need a genuinely free production tier ImageKit “Forever Free”: 20 GB bandwidth, 3 GB DAM storage. Bunny and imgix have trials only; Cloudflare’s free tier covers transformations but not storage or delivery
You need a flat, predictable per-site bill Bunny Optimizer $9.50/website with unlimited transformations — as long as you budget CDN bandwidth separately, which is the trap in trap #2 above
You need an MCP server today ImageKit, Cloudinary or Cloudflare All three ship one; we do not
You need per-key usage attribution inside one tenant nitida usage.keys() attributes storage and traffic to the key that caused it. Cloudinary and ImageKit report per account; imgix per Source; Vercel’s finest grain is the team
You need storage bytes and file count as a first-class number per tenant nitida or Bunny Bunny returns StorageUsed + FilesStored from one authenticated GET per zone — genuinely good, and the closest competitor on this axis
You want one URL per asset that survives every resize, format and crop nitida Content addressing is the whole design: the SHA is the asset and every URL is a function of it
  • It does not compare latency or throughput. Nobody ran a delivery benchmark across these six.
  • It does not compare image quality at equal settings. Our own encoder is measured in the transform benchmark; theirs is not, and a cross-vendor quality claim without a measurement is the same fabrication as a normalised price column.
  • It does not compare support, SLA or contract terms beyond noting that we have none.

Every competitor claim on this page was verified on 2026-08-17 against the vendor’s own documentation, at the URL stamped next to it in this page’s source.

Freshness is enforced mechanically, not by good intentions: A freshness check parses every stamp on every CI run and fails the build when one is older than 90 days, carries no date, carries no URL, or is dated in the future. It deliberately does not fetch the competitor URLs: a 200 proves the page still exists, not that the sentence we quoted is still on it — so an automated fetch would report a safety it never verified. Re-reading is a human step, on purpose.

  1. Vercel never writes the sentence “no video”. What it writes is the format allow-list: “A source image must be one of the following formats to be optimized: image/jpeg, image/png, image/webp, image/avif. Other formats will be served as-is”. So the honest phrasing is images only, and everything else passes through untouched — not “video is unsupported”, which is an inference we would be putting in their mouth. ↩

  2. With a caveat Cloudinary states itself, for authenticated assets: “You cannot create on-the-fly transformations; only pre-generated eager transformations work.” ↩

  3. The canonical page imagekit.io/docs/security/signed-urls returns 404. The quoted behaviour is official content reached through their own search, but the exact URL is unverified, so it is not stamped as a source. ↩

  4. Searched for a signing mechanism; found only the remotePatterns / localPatterns allow-list. An allow-list is access control over sources, not a signature over a URL, and the two are not interchangeable. ↩

  5. Searched “api key” and “per user” across the full 394 KB of admin_api.md. Credentials appear; attribution does not. Cloudinary sells tenant separation as separate accounts (“1 Account” / “2 Accounts” / “3 Accounts”), not as per-key metering. ↩

  6. Searched the Images and Workers-binding documentation for a bytes-plus-file-count-by-key endpoint: zero results. ↩

  7. A community bunnycdn-mcp exists. It is not official and is therefore not counted as a Bunny feature here. ↩

  8. Searched imgix.com and docs.imgix.com for an MCP server and for llms.txt; found neither. Their “Generative AI Endpoint” is media generation, not an agent surface, and conflating the two would be the easy mistake. ↩

  9. A GET /usage on a Basic plan returns image_max_size_bytes: 157286400, video_max_size_bytes: 3145728000, image_max_px: 100000000 — but those are that plan’s values, read programmatically. The Free and Plus figures are published only in a support article that returns 403, so they are not stated here. ↩

  10. Bunny publishes the opposite — “Unlimited requests · Unlimited optimizations · Unlimited transformations”. That is a statement about billing, not a technical ceiling, and reading it as “no limits” is exactly the error this footnote exists to prevent. ↩