# How this compares

> nitida against Cloudinary, Cloudflare, Bunny, imgix, ImageKit and Vercel — every competitor number quoted from their own docs with the date, every row we could not verify marked as unverified, and the cases where you should use one of them instead.

{/*
  ⚠️ EVERY competitor claim on this page carries a machine-readable stamp: an
  MDX comment holding the keyword "verifi" + "ed:", then an ISO date, then the
  URL it was read from. (The keyword is split here on purpose — this very
  paragraph would otherwise parse as a stamp with no date, which is how the
  guard was first proved to work.) Look at any stamp below for the real shape.

  A freshness check in CI parses them and fails the build when a stamp
  is older than 90 days, has no date, has no URL, or carries a date in the
  future. Do not add a competitor row without a stamp, and do not edit a stamp's
  date without re-reading the page it points at.
*/}

## How this page is sourced

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](/guides/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.

## Where nitida is the wrong choice

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.

{/* verified: 2026-08-17 https://bunny.net/pricing/ */}

**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.

{/* verified: 2026-08-17 https://imagekit.io/plans */}

**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.

{/* verified: 2026-08-17 https://cloudinary.com/documentation/upload_images */}

**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.

{/* verified: 2026-08-17 https://bunny.net/pricing/stream/ */}

**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. {/* verified: 2026-08-17 https://cloudinary.com/documentation/cloudinary_llm_mcp */}
- 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()`". {/* verified: 2026-08-17 https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/ */}
- 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`.
  {/* verified: 2026-08-17 https://imagekit.io/docs/mcp-server */}

What we have instead is a typed SDK, `llms.txt`, a `.md` route on every page of this site and the
[agent files](/start/for-agents/). 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.

{/* verified: 2026-08-17 https://docs.imgix.com/getting-started/setup/creating-sources */}
{/* verified: 2026-08-17 https://imagekit.io/docs/integration/connect-external-storage */}

## 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](/start/what-is-this/) |

{/* verified: 2026-08-17 https://cloudinary.com/documentation/fetch_remote_images */}
{/* verified: 2026-08-17 https://developers.cloudflare.com/images/pricing/ */}
{/* verified: 2026-08-17 https://bunny.net/optimizer/ */}
{/* verified: 2026-08-17 https://docs.imgix.com/getting-started/setup/creating-sources */}
{/* verified: 2026-08-17 https://imagekit.io/docs/integration/connect-external-storage */}
{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization */}

**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.

:::note[Vercel's cache is not storage, and the docs are explicit about the expiry]
"Local image cache expiration: Cached **for up to 31 days** on the Vercel CDN." For remote images
the TTL "is determined by the `Cache-Control` `max-age` header from the upstream image or
`minimumCacheTTL` config (**default: 3600 seconds**), whichever is larger." So a variant you want
in six months re-fetches your origin — your origin has to still be there, and still be serving it.
{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization */}
:::

## 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](/start/credentials/) | — |

{/* verified: 2026-08-17 https://cloudinary.com/pricing */}
{/* verified: 2026-08-17 https://developers.cloudflare.com/images/pricing/ */}
{/* verified: 2026-08-17 https://developers.cloudflare.com/stream/pricing/ */}
{/* verified: 2026-08-17 https://bunny.net/pricing/optimizer/ */}
{/* verified: 2026-08-17 https://bunny.net/pricing/ */}
{/* verified: 2026-08-17 https://bunny.net/pricing/storage/ */}
{/* verified: 2026-08-17 https://imgix.com/pricing */}
{/* verified: 2026-08-17 https://docs.imgix.com/en-US/references/billing-overview */}
{/* verified: 2026-08-17 https://imagekit.io/plans */}
{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization/limits-and-pricing */}

### Three ways this table is usually wrong

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.

{/* verified: 2026-08-17 https://developers.cloudflare.com/images/pricing/ */}
{/* verified: 2026-08-17 https://bunny.net/optimizer/ */}
{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization/limits-and-pricing */}

### 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."

{/* verified: 2026-08-17 https://cloudinary.com/documentation/transformation_counts */}

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."

{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization/limits-and-pricing */}

## Feature matrix

`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](/start/for-agents/) — **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](/concepts/variants-and-presets/) |

{/* verified: 2026-08-17 https://cloudinary.com/documentation/adaptive_bitrate_streaming */}
{/* verified: 2026-08-17 https://cloudinary.com/documentation/control_access_to_media */}
{/* verified: 2026-08-17 https://cloudinary.com/documentation/admin_api */}
{/* verified: 2026-08-17 https://cloudinary.com/documentation/upload_images */}
{/* verified: 2026-08-17 https://developers.cloudflare.com/images/get-started/limits/ */}
{/* verified: 2026-08-17 https://developers.cloudflare.com/stream/pricing/ */}
{/* verified: 2026-08-17 https://bunny.net/docs/cdn/security/token-authentication */}
{/* verified: 2026-08-17 https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone */}
{/* verified: 2026-08-17 https://bunny.net/blog/introducing-the-bunny-net-cli/ */}
{/* verified: 2026-08-17 https://docs.imgix.com/setup/securing-images */}
{/* verified: 2026-08-17 https://docs.imgix.com/en-US/apis/video/overview */}
{/* verified: 2026-08-17 https://docs.imgix.com/en-US/apis/management/reports */}
{/* verified: 2026-08-17 https://docs.imgix.com/apis/rendering */}
{/* verified: 2026-08-17 https://imagekit.io/docs/adaptive-bitrate-streaming */}
{/* verified: 2026-08-17 https://imagekit.io/docs/transformations */}
{/* verified: 2026-08-17 https://imagekit.io/docs/mcp-server */}
{/* verified: 2026-08-17 https://vercel.com/docs/image-optimization */}

[^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.

## ⭐ The dedup row, worded precisely

**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". {/* verified: 2026-08-17 https://cloudinary.com/documentation/image_upload_api_reference */}
- **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.
  {/* verified: 2026-08-17 https://developers.cloudflare.com/images/pricing/ */}
- **Bunny** — storage is a path-indexed, filesystem-shaped zone, which argues against it. Bunny
  says neither. {/* verified: 2026-08-17 https://bunny.net/pricing/storage/ */}
- **imgix** — the closest thing is one-origin-many-derivatives, which is not deduplication.
  {/* verified: 2026-08-17 https://docs.imgix.com/getting-started/setup/creating-sources */}
- **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.
  {/* verified: 2026-08-17 https://imagekit.io/docs/transformations */}
- **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.
  {/* verified: 2026-08-17 https://vercel.com/docs/image-optimization */}

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](/guides/transform-benchmark/) relies on it — it uploads the same photograph
twice on purpose and the second upload deduplicates.

## If you need X, use Y

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 |

## What this page does not claim

- 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](/guides/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.