Skip to main content

Cloudinary vs Uploadthing vs ImageKit: Image Storage and CDN in 2026

·PkgPulse Team

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

PackageWeekly DownloadsCategory
cloudinary~530KNode.js SDK
next-cloudinary~120KNext.js component
uploadthing~220KType-safe uploads
imagekit~80KNode.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/image for 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

FeatureCloudinaryUploadthingImageKit
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 tier25 credits/mo2GB storage20GB 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/image for 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.

Compare image SDK packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.