Cloudinary vs Uploadthing vs ImageKit: Image Storage and CDN in 2026
TL;DR
For image handling in 2026: Uploadthing is the best choice for Next.js and TypeScript projects — type-safe, serverless-first, and designed for modern app stacks with minimal configuration. Cloudinary is the enterprise choice with the most comprehensive transformation API and CDN. ImageKit is the best value — real-time image optimization at a fraction of Cloudinary's price, with a free tier that scales reasonably.
Key Takeaways
- Cloudinary: The most feature-complete — 500+ image/video transformations, AI-powered features
- Uploadthing: Best DX for Next.js — type-safe upload routes, built-in auth, minimal setup
- ImageKit: Best value — similar CDN performance to Cloudinary at 10-50% of the cost
- All three serve images via CDN with automatic format detection (WebP/AVIF)
- Uploadthing wins for developer simplicity; Cloudinary wins for transformation power
- For most startups: ImageKit free tier (20GB) + Next.js Image component is sufficient
Download Trends
| Package | Weekly Downloads | Category |
|---|---|---|
cloudinary | ~530K | Node.js SDK |
next-cloudinary | ~120K | Next.js component |
uploadthing | ~220K | Type-safe uploads |
imagekit | ~80K | Node.js SDK |
The Image Optimization Pipeline
All three services handle this pipeline on their infrastructure:
User uploads image
↓
Upload to CDN storage (S3-backed)
↓
On-demand transformation (resize, crop, format convert, optimize)
↓
Serve from edge CDN (closest server to user)
↓
Cache transformed image for future requests
The differences are in: developer API, transformation capabilities, pricing, and ecosystem integrations.
Cloudinary
Cloudinary is the industry standard for media management:
import { v2 as cloudinary } from "cloudinary"
import { UploadApiResponse } from "cloudinary"
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
})
// Upload a file:
async function uploadPackageScreenshot(filePath: string, packageName: string): Promise<string> {
const result: UploadApiResponse = await cloudinary.uploader.upload(filePath, {
folder: `packages/${packageName}`,
public_id: "screenshot",
overwrite: true,
resource_type: "image",
transformation: [
{ width: 1200, height: 630, crop: "fill", gravity: "center" },
{ quality: "auto", fetch_format: "auto" }, // Auto WebP/AVIF
],
tags: ["package-screenshot", packageName],
})
return result.secure_url
}
// Generate transformation URLs (without re-uploading):
function getOptimizedUrl(publicId: string, width: number, height?: number): string {
return cloudinary.url(publicId, {
transformation: [
{ width, height, crop: height ? "fill" : "scale" },
{ quality: "auto:best", fetch_format: "auto" },
{ dpr: "auto" }, // Serve 2x for retina
],
secure: true,
})
}
// Advanced transformations:
const overlayUrl = cloudinary.url("package-card", {
transformation: [
{ width: 1200, height: 630 },
// Overlay text:
{
overlay: { font_family: "Inter", font_size: 48, font_weight: "bold", text: "react" },
color: "#ffffff",
gravity: "north_west",
x: 60,
y: 60,
},
// Overlay an icon:
{
overlay: "npm-logo",
width: 80,
gravity: "south_east",
x: 30,
y: 30,
},
{ quality: "auto", fetch_format: "auto" },
],
})
Cloudinary's transformation API power:
URL-based transformations — no upload required for new sizes:
https://res.cloudinary.com/demo/image/upload/
w_400,h_300,c_fill,g_face/ ← Resize to 400×300, crop to face
e_grayscale/ ← Apply grayscale
e_blur:200/ ← Apply blur
q_auto,f_auto/ ← Auto quality and format
sample.jpg
next-cloudinary (Next.js component):
import { CldImage, CldUploadButton } from "next-cloudinary"
// Drop-in replacement for next/image with Cloudinary optimization:
function PackageCard({ imagePublicId }: { imagePublicId: string }) {
return (
<CldImage
src={imagePublicId}
width={800}
height={450}
alt="Package screenshot"
crop="fill"
gravity="center"
sizes="(max-width: 768px) 100vw, 800px"
/>
)
}
// Upload widget (no backend needed):
function UploadButton({ onUpload }: { onUpload: (url: string) => void }) {
return (
<CldUploadButton
uploadPreset="pkgpulse-screenshots"
onSuccess={(result) => {
if (result.info && typeof result.info === "object") {
onUpload(result.info.secure_url)
}
}}
>
Upload Screenshot
</CldUploadButton>
)
}
Cloudinary pricing: Free tier = 25 credits/month (~25GB storage + 25GB bandwidth). Paid from $89/month. Expensive at scale.
Uploadthing
Uploadthing was built specifically for Next.js and modern TypeScript stacks:
// app/api/uploadthing/core.ts — Define upload routes:
import { createUploadthing, type FileRouter } from "uploadthing/next"
import { getServerSession } from "next-auth"
const f = createUploadthing()
export const ourFileRouter = {
// Package screenshot upload (authenticated):
packageScreenshot: f({
image: {
maxFileSize: "4MB",
maxFileCount: 1,
},
})
.middleware(async ({ req }) => {
const session = await getServerSession()
if (!session?.user) throw new Error("Unauthorized")
// Return metadata that becomes available in onUploadComplete:
return { userId: session.user.id }
})
.onUploadComplete(async ({ metadata, file }) => {
// Save to database:
await db.packageImage.create({
data: {
url: file.url,
key: file.key,
uploadedBy: metadata.userId,
},
})
// Return data to the client:
return { uploadedBy: metadata.userId, url: file.url }
}),
// Bulk imports (multiple files, higher limits):
csvImport: f({ text: { maxFileSize: "16MB", maxFileCount: 1 } })
.middleware(async ({ req }) => {
const session = await getServerSession()
if (!session?.user?.isAdmin) throw new Error("Admin only")
return { userId: session.user.id }
})
.onUploadComplete(async ({ file }) => {
await processCsvImport(file.url)
return { processed: true }
}),
} satisfies FileRouter
export type OurFileRouter = typeof ourFileRouter
// app/api/uploadthing/route.ts:
import { createRouteHandler } from "uploadthing/next"
import { ourFileRouter } from "./core"
export const { GET, POST } = createRouteHandler({ router: ourFileRouter })
// Client component — type-safe upload:
"use client"
import { useUploadThing } from "~/utils/uploadthing"
function PackageScreenshotUploader() {
const { startUpload, isUploading } = useUploadThing("packageScreenshot", {
onClientUploadComplete: (res) => {
console.log("Uploaded:", res?.[0].url)
// res is fully typed based on your router definition
},
onUploadError: (error) => {
console.error("Upload error:", error.message)
},
})
const handleDrop = (files: File[]) => {
startUpload(files)
}
return (
<div onDrop={(e) => handleDrop([...e.dataTransfer.files])}>
{isUploading ? "Uploading..." : "Drop image here"}
</div>
)
}
Uploadthing's design philosophy:
- File router definitions are just TypeScript — no config files, no UI
- Middleware has access to your session/auth context directly
- Client hooks are generated from your router type — fully type-safe
- Files are stored on Uploadthing's S3 infrastructure with CDN delivery
- No CDN image transformation — use
next/imagefor optimization
Uploadthing pricing: Free tier = 2GB storage. Paid from $10/month. Very affordable for small apps.
ImageKit
ImageKit is the Cloudinary alternative with better pricing:
import ImageKit from "imagekit"
import { UploadResponse } from "imagekit/dist/libs/interfaces"
const imagekit = new ImageKit({
publicKey: process.env.IMAGEKIT_PUBLIC_KEY!,
privateKey: process.env.IMAGEKIT_PRIVATE_KEY!,
urlEndpoint: process.env.IMAGEKIT_URL_ENDPOINT!,
})
// Upload:
async function uploadImage(buffer: Buffer, fileName: string): Promise<string> {
const result: UploadResponse = await imagekit.upload({
file: buffer,
fileName,
folder: "/packages",
tags: ["package-image"],
useUniqueFileName: true,
transformation: {
pre: "w-1200,h-630,cm-pad_resize",
},
})
return result.url
}
// URL transformation (same as Cloudinary's URL approach):
function getResizedUrl(imageUrl: string, width: number, quality = 80): string {
// Append transformation parameters to URL:
return `${imageUrl}?tr=w-${width},q-${quality},f-auto`
}
// Or use ImageKit's URL builder:
function buildTransformedUrl(imageId: string, transforms: object): string {
return imagekit.url({
path: imageId,
transformation: [transforms],
})
}
ImageKit URL transformation syntax:
https://ik.imagekit.io/demo/image.jpg
?tr=w-400,h-300,c-maintain_ratio ← Resize maintaining ratio
:w-400,h-300,cm-extract,x-100,y-50 ← Chained transformations
Built-in transformations:
- w (width), h (height)
- c (crop: maintain_ratio, force, at_least, at_max)
- cm (crop mode: extract, pad_resize, pad_extract)
- f (format: auto, webp, avif, jpg, png)
- q (quality: 0-100 or auto)
- bl (blur), rt (rotate), flip-h, flip-v
- e-sharpen, e-grayscale, e-contrast
- lo-true (lossless)
Next.js Image with ImageKit:
// next.config.ts:
export default {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "ik.imagekit.io",
},
],
},
}
// Component — just use next/image normally:
import Image from "next/image"
function PackageImage({ src }: { src: string }) {
return (
<Image
src={src}
width={800}
height={450}
alt="Package screenshot"
// next/image handles srcset, WebP conversion, and lazy loading
// ImageKit handles CDN delivery from the URL
/>
)
}
ImageKit pricing: Free tier = 20GB bandwidth/month + 20GB storage. Paid from $49/month (vs Cloudinary's $89/month). Roughly 40-50% cheaper than Cloudinary for equivalent features.
Feature Comparison
| Feature | Cloudinary | Uploadthing | ImageKit |
|---|---|---|---|
| Image transformations | ⭐⭐⭐⭐⭐ 500+ | ❌ (use next/image) | ⭐⭐⭐⭐ 100+ |
| Video transformations | ✅ | ❌ | ✅ |
| TypeScript DX | ⚠️ Average | ✅ Excellent | ✅ Good |
| Next.js integration | ✅ next-cloudinary | ✅ Native | ✅ Via remotePatterns |
| Type-safe file router | ❌ | ✅ | ❌ |
| AI background removal | ✅ | ❌ | ✅ |
| CDN delivery | ✅ | ✅ | ✅ |
| Free tier | 25 credits/mo | 2GB storage | 20GB bandwidth |
| Pricing | $$$ | $ | $$ |
| Upload widget | ✅ | ✅ | ✅ |
| Signed uploads | ✅ | ✅ (middleware) | ✅ |
When to Use Each
Choose Cloudinary if:
- You need 500+ transformations (generative AI, gravity, overlays)
- Video transformation is required
- Enterprise team with Cloudinary expertise already
- Budget is not a constraint
Choose Uploadthing if:
- Building with Next.js App Router and TypeScript
- Simplest possible setup is the priority
- Type-safe upload routes with auth middleware appeal to you
- You'll use
next/imagefor optimization (Uploadthing is storage, not CDN transformer)
Choose ImageKit if:
- You want Cloudinary-level CDN and transformations at lower cost
- 20GB free tier is sufficient for your stage
- Migration from Cloudinary is being considered
Methodology
Pricing data from official pages (March 2026). Download data from npm registry (weekly average, February 2026). Feature comparison based on Cloudinary Node.js SDK v2, Uploadthing v7, and ImageKit Node.js SDK v5.