← Back to blog
··14 min read

UploadThing vs Cloudinary vs AWS S3 in 2026: Which File Storage Should You Use

UploadThingCloudinaryAWS S3File UploadsStorageNext.jsSaaS
UploadThing vs Cloudinary vs AWS S3 in 2026: Which File Storage Should You Use

The Question Behind the Question

Every real app eventually has to accept a file. A user uploads an avatar, a document, a product photo, a video, an invoice, a CSV. The moment you add that feature, you face a decision that looks small and turns out to shape your infrastructure, your bill, and your security posture: where do the files go, and how do they get there?

In 2026 the three names that come up most for a modern Next.js or TypeScript app are UploadThing, Cloudinary, and AWS S3. They get compared as if they're direct competitors. They aren't — not really. They live at different layers of the same stack, and understanding that is the whole game.

  • AWS S3 is raw object storage. A bucket. It stores a file and gives it a URL. Everything else — accepting the upload, securing it, putting a CDN in front, processing images — is yours to build.
  • Cloudinary is a media platform: storage plus a transformation-and-delivery pipeline. Upload an image once, request any variant through the URL, and it generates and caches it for you.
  • UploadThing is an upload service: a developer-experience layer that sits on top of storage and hands you the Next.js integration — typed client, prebuilt components, server-side rules — so you skip the infrastructure entirely.
  • Put simply: S3 is what you build on. Cloudinary is storage with a media brain. UploadThing is uploads-in-an-afternoon. The rest of this guide is about matching the right layer to your actual problem.

    Cloud infrastructure and global network of connected storage nodes

    Cloud infrastructure and global network of connected storage nodes

    How File Uploads Actually Work

    Before comparing tools, it helps to see the pattern all three are solving, because it explains why they make the trade-offs they do.

    A naive upload sends the file to your app server, which then forwards it to storage. This is a bad idea at scale: your server becomes a bottleneck for every large file, and you pay to move bytes twice. The pattern every mature setup uses instead is the presigned URL (also called a signed URL):

  • The browser asks your server for permission to upload.
  • Your server — which holds the secret storage credentials — checks the rules (is this user allowed, is the file type and size OK) and generates a short-lived URL that grants permission to upload exactly one file for a few minutes.
  • The browser uploads the file directly to storage using that URL. Your credentials never touch the client.
  • Storage confirms; your server saves the file's URL or key in your database.
  • This keeps secret keys on the server, enforces your rules before anything is written, and takes your app server out of the data path. Whether you use S3, Cloudinary, or UploadThing, this is roughly what's happening underneath — the difference is how much of it you write yourself.

  • With plain S3/R2, you write it: presigned URLs via the AWS SDK, CORS config on the bucket, a CDN in front, and any image processing.
  • With UploadThing, the service does it: your file router is where you set the rules, and the presigned upload plus callback are handled for you.
  • With Cloudinary, signed uploads and access control are built in, and the media pipeline is the point.
  • UploadThing: Uploads in an Afternoon

    UploadThing exists to answer one frustration: adding file uploads to a Next.js app used to take a day of AWS plumbing for a feature that should take an hour. It collapses that into two pieces.

    On the server, you define a file router — a typed description of each upload endpoint:

    ts
    // app/api/uploadthing/core.ts
    import { createUploadthing, type FileRouter } from "uploadthing/next";
    
    const f = createUploadthing();
    
    export const uploadRouter = {
      avatar: f({ image: { maxFileSize: "2MB", maxFileCount: 1 } })
        .middleware(async ({ req }) => {
          const user = await auth(req); // your auth
          if (!user) throw new Error("Unauthorized");
          return { userId: user.id }; // passed to onUploadComplete
        })
        .onUploadComplete(async ({ metadata, file }) => {
          // runs on YOUR server after the file lands
          await db.user.update({
            where: { id: metadata.userId },
            data: { image: file.url },
          });
        }),
    } satisfies FileRouter;
    
    export type OurFileRouter = typeof uploadRouter;

    On the client, you drop in a prebuilt, fully typed component already wired to that router:

    tsx
    "use client";
    import { UploadButton } from "@/lib/uploadthing";
    
    export function AvatarUpload() {
      return <UploadButton endpoint="avatar" />;
    }

    That's the whole feature. No bucket, no IAM policy, no signed-URL code, no CORS. The middleware is exactly where you enforce who can upload, and onUploadComplete is where you save the result. The end-to-end type safety — the client endpoint names are inferred from the server router — is the kind of DX that makes it feel like the rest of a modern TypeScript stack.

    Where it shines: moderate-volume apps that want uploads working today, avatars/documents/attachments, and teams that value Next.js developer experience over squeezing cost at scale.

    The trade-offs: you're paying for a managed service above commodity storage, so cost-per-gigabyte is higher than raw S3/R2 and matters more as you grow; you have less low-level control; and it's an upload layer, not a media-transformation platform — heavy on-the-fly image/video processing isn't its job.

    Cloudinary: When the Media Is the Product

    Cloudinary's pitch is different: it assumes the images and videos are the product, and it owns the hard media work so you never build an image pipeline.

    The magic is URL-based transformation. You upload an original once, then request any derivative by changing the URL:

    https://res.cloudinary.com/demo/image/upload/w_400,h_400,c_fill,g_auto,f_auto,q_auto/photo.jpg

    That single URL says: resize to 400×400, c_fill crop, g_auto smart-crop to keep the subject centered, f_auto pick the best format (WebP/AVIF) for the browser, q_auto auto-compress. Cloudinary generates it on demand, caches it on a global CDN, and serves it fast. You get responsive images at any size, format conversion, watermarks, overlays, and even AI cropping — without building or running a resizing service. It handles video too: transcoding, adaptive streaming, and poster frames that plain S3 can't do alone.

    Where it shines: e-commerce catalogs, marketplaces and social platforms drowning in user-generated media, publishing sites needing responsive images, and any product where automatic image optimization directly moves page speed and conversion. If you're building an e-commerce or media-heavy marketplace app, this is the category that saves you the most work.

    The trade-offs: cost and lock-in. Pricing bundles storage + transformations + bandwidth (often as credits) and can climb fast on high-traffic media sites, and because your image logic lives in Cloudinary's URL syntax, migrating away later is real work.

    Rows of servers in a data center powering object storage at scale

    Rows of servers in a data center powering object storage at scale

    AWS S3: The Standard Everything Is Built On

    S3 is the foundation. It's durable, effectively unlimited object storage priced per gigabyte stored plus requests and egress — and it's the substrate a huge share of the internet's files sit on, including the backends of many upload and media services. Its data model and API are so ubiquitous that "use S3" often really means "use the S3 protocol" and choose your host.

    That last point is the most important thing to understand about S3 in 2026: the S3 API is a de facto standard, implemented by:

  • Cloudflare R2 — S3-compatible with zero egress fees, which is why it's become the go-to for serving large volumes of user files cheaply.
  • Backblaze B2, DigitalOcean Spaces, MinIO (self-hosted), and others — same protocol, different pricing.
  • Supabase Storage — S3-compatible object storage that pairs naturally if you're already on Supabase.
  • So the practical decision is rarely "S3 or not" — it's "the S3 protocol, hosted where the pricing fits." A minimal presigned upload with the AWS SDK looks like this (and works against R2 by swapping the endpoint):

    ts
    import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
    import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
    
    const s3 = new S3Client({ region: "auto" /* R2: set endpoint */ });
    
    export async function createUploadUrl(key: string, type: string) {
      const command = new PutObjectCommand({
        Bucket: process.env.S3_BUCKET,
        Key: key,
        ContentType: type,
      });
      // short-lived URL the browser uploads to directly
      return getSignedUrl(s3, command, { expiresIn: 60 });
    }

    Where it shines: cheap storage at scale, maximum flexibility, minimum lock-in, and a model every developer and tool already knows. Choose R2 specifically when egress bandwidth would otherwise make AWS S3 expensive.

    The trade-offs: you own the plumbing — presigned URLs, CORS, bucket policies, a CDN (CloudFront or Cloudflare) for fast global delivery, and any image processing (build it, or put Cloudinary/an image CDN on top).

    Head-to-Head

    UploadThingCloudinaryAWS S3 (+ R2)
    **Layer**Upload service (DX layer)Media platform (storage + pipeline)Raw object storage
    **Best at**Fast Next.js integrationImage/video transformationCheap, standard, portable storage
    **Next.js DX**Excellent — typed, prebuilt UIGood SDKs + URL transformsManual (SDK + presigned URLs)
    **Image processing**MinimalBest-in-class, on-the-flyBuild it yourself
    **Cost model**Managed plans (premium for DX)Storage + transforms + bandwidthPer-GB + requests + egress
    **Cost at scale**HigherCan climb on high trafficCheapest (R2 kills egress fees)
    **Lock-in**Service-levelHigh (URL/transform syntax)Low (S3 API is a standard)
    **You own**Almost nothingLittle (they run the pipeline)Uploads, CDN, processing
    **Ship it when**Uploads today, moderate volumeMedia is the productYou want standard, portable storage

    They're Not Mutually Exclusive

    The comparison framing hides the most common production answer: combine them. A very typical setup stores originals in S3 or R2 (cheap, durable, portable) and puts an image CDN or Cloudinary in front for transformation and delivery. Another stores files with UploadThing early for speed, then migrates the storage layer later once volume justifies owning a bucket. The layers stack precisely because they solve different problems — don't feel forced to pick exactly one.

    Security Fundamentals (All Three)

    Whichever you choose, the same rules keep uploads safe — and they're the same rules that make any codebase read as production-ready:

  • Use presigned URLs. Never expose long-lived storage credentials to the browser, and don't route big files through your app server.
  • Validate on the server, not just the client. Check file type and size before issuing the URL — client checks are a UX nicety, not security.
  • Default to private. Make anything sensitive private and grant access with signed download URLs; make files public only where you intend to.
  • Be careful with user HTML/SVG. Serving user-uploaded SVG or HTML from your domain is an XSS vector — sanitize or serve from a separate origin.
  • Store the URL/key in your database, not the bytes. Your database holds a reference; the file lives in storage.
  • Set limits and scan. Enforce max size and count; scan for malware if you accept arbitrary files.
  • UploadThing hands you most of this by default (the file router is the enforcement point). With S3/R2 you implement it. With Cloudinary you configure signed uploads and access control. None of them make the fundamentals optional.

    Developer writing TypeScript that wires an upload endpoint to storage

    Developer writing TypeScript that wires an upload endpoint to storage

    What This Means If You Build to Sell

    If you're packaging a SaaS starter kit, Next.js boilerplate, or Stripe-powered subscription app to sell on CodeCudos, your file-storage choice signals a lot about the codebase — and about what the buyer inherits the moment they clone it. Buyers notice:

  • Never hardcode storage credentials. Access keys, account IDs, and bucket names belong in environment variables with an example env file — never committed into a repo you sell. This is the single fastest way a codebase reads as amateur.
  • Make the provider swappable. Put storage behind a thin interface where practical so the buyer isn't married to your account or your vendor's pricing. Shipping against the S3 API is the most portable default — the buyer points it at AWS, R2, or any compatible host with env-var changes only.
  • Ship safe defaults. Sensible file-type and size limits plus signed-URL security mean the buyer inherits a safe configuration, not an open write endpoint that anyone can dump files into.
  • Document the storage. State exactly which service the app expects, how to create the equivalent bucket or app, and how to plug in their own keys — the same clarity you'd want for the database or auth layer.
  • For most templates, either UploadThing (for clean Next.js DX the buyer can re-point in minutes) or the S3 API (for maximum portability and zero lock-in) is the resale-safe default. Ship Cloudinary only when the template is genuinely media-centric — and say so, so no buyer is surprised by a metered media bill under what they thought was simple storage. These are the same standards that make any code read as production-ready, and they compound with the rest of a credible build: a coherent tech stack, a well-chosen database, and a sensible host.

    The Bottom Line

    There's no universal winner — there's a right layer for your problem, your ecosystem, and your scale.

  • "I want file uploads working today in a Next.js app, with great DX" → UploadThing
  • "The images and video are the product and need heavy transformation" → Cloudinary
  • "I want cheap, standard, portable object storage I fully control" → AWS S3 — or Cloudflare R2 if egress bandwidth would otherwise hurt
  • "I need cheap originals plus automatic image optimization" → S3/R2 for storage with Cloudinary or an image CDN in front
  • "A boilerplate I'll sell that must read as clean and mainstream" → UploadThing or the S3 API, credentials in env vars, provider swappable
  • Whichever you choose, the habit that outlasts the decision is the same: use presigned URLs, validate on the server, default to private, keep credentials in env vars, store references in your database, and keep the storage layer genuinely production-ready. That discipline costs little and pays back on every file your users upload — and every buyer who clones your repo.

    Ready to turn what you build into income? List your SaaS or template on CodeCudos, see how storage fits the wider stack in our best tech stack for web apps in 2026 guide, pick your database with PostgreSQL vs MySQL vs MongoDB, choose a host with Vercel vs Netlify vs Railway, or make sure the whole build reads as production-ready.

    Frequently asked questions

    What is the real difference between an upload service, a media platform, and object storage?

    It's the fork that decides everything else, and the three tools sit at different layers of the same stack. Object storage — AWS S3 is the definitive example, with Cloudflare R2, Backblaze B2, Google Cloud Storage, and Supabase Storage as close relatives — is the lowest level: a bucket that stores files (objects) by key, gives each one a URL, and does essentially nothing else. It's cheap, infinitely scalable, and battle-tested, but accepting an upload from a browser, securing it, putting a CDN in front, and processing images are all your job. A media platform — Cloudinary is the archetype — is storage plus a transformation and delivery pipeline built on top: you upload an image or video once and then request any variant of it through the URL (a 400px WebP, a smart-cropped thumbnail, a compressed video poster) and Cloudinary generates and caches it for you, so the platform, not your code, owns the hard media work. An upload service — UploadThing is the clearest example — is a developer-experience layer that sits above storage: it gives you a typed API, ready-made upload UI components, and server-side rules about who can upload what and how big, so you get working, secure uploads in a Next.js app without provisioning buckets or writing signed-URL logic. So the split is: S3 equals raw storage you build on; Cloudinary equals storage plus a media pipeline; UploadThing equals a batteries-included upload layer that hides the storage entirely. Many real apps combine them.

    Is UploadThing really the fastest to ship, and what's the catch?

    For a Next.js or React app, yes — UploadThing is built to get file uploads working faster than any of the alternatives, and that's its whole reason to exist. It comes from the same ecosystem as the modern TypeScript tooling many developers already use, and it leans hard into developer experience: you define a file router on the server that describes each upload endpoint (max size, file types, who's allowed, and what to do after upload), and you drop in prebuilt UploadButton or UploadDropzone components on the client that are already wired to it and fully typed end-to-end. There are no buckets to create, no IAM policies, no signed-URL code, no CORS wrestling — the service handles storage, the presigned upload, and delivery, and you get a clean callback on your server when a file lands so you can save its URL to your database. The catches are the usual trade-offs of a managed abstraction. You're depending on a third-party service and its pricing rather than commodity storage, so cost per gigabyte is higher than raw S3/R2 and can matter a lot at scale; you have less low-level control than owning a bucket; and while UploadThing handles storage and basic needs well, it is an upload layer, not a full media-transformation platform like Cloudinary, so heavy on-the-fly image and video processing isn't its core job. The honest summary: UploadThing is the best choice when speed of integration and Next.js developer experience matter most and your volumes are moderate; it's less ideal when you're extremely cost-sensitive at large scale or need deep media transformation.

    When is Cloudinary the right choice over the others?

    Reach for Cloudinary when the media is the product, not an afterthought. Its superpower is on-the-fly transformation: you upload an original image or video once, and then you request any derivative of it simply by changing the URL — a specific width and height, a different crop, a format like WebP or AVIF, a quality/compression level, a blur, an overlay or watermark, or an AI-driven smart crop that keeps the subject centered. Cloudinary generates each variant on demand, caches it on a global CDN, and serves it fast, which means you never build or maintain an image-resizing pipeline, a thumbnail generator, or a format-conversion step yourself. That makes it the natural fit for image-heavy and video-heavy apps: e-commerce catalogs with dozens of product shots per item, marketplaces and social platforms with endless user-generated media, publishing sites that need responsive images at many sizes, or any product where automatic optimization directly affects page speed and conversion. It also handles video transcoding, adaptive streaming, and video thumbnails, which S3 alone does not. The trade-offs are cost and lock-in: Cloudinary's pricing is based on storage plus transformations plus bandwidth (often metered as credits), which can climb quickly on high-traffic media sites, and because your image logic lives in Cloudinary URLs and its transformation syntax, migrating away later is real work. The rule of thumb: choose Cloudinary when rich, automated image/video handling is central to the app and worth paying for; if you mostly need to store and serve files without heavy transformation, a simpler upload layer or plain object storage is cheaper and less entangling.

    When should I just use AWS S3 (or an S3-compatible store like Cloudflare R2)?

    Use S3 or an S3-compatible store when you want the industry-standard foundation: the cheapest storage at scale, the most flexibility, the least lock-in, and a data model every developer and tool already understands. S3 is raw object storage — durable, effectively unlimited, and priced per gigabyte stored plus requests and egress bandwidth — and it's the substrate a huge portion of the internet's files sit on, including the backends of many other upload and media services. That ubiquity is the point: skills, tooling, and documentation are everywhere, and the S3 API is a de facto standard implemented by alternatives like Cloudflare R2, Backblaze B2, DigitalOcean Spaces, and MinIO, so use S3 often really means use the S3 protocol and pick the host with the pricing you want. That matters because AWS charges for egress (bandwidth out), which can dominate the bill on media-heavy apps — which is exactly why Cloudflare R2, with zero egress fees and an S3-compatible API, has become a popular choice for storing and serving large amounts of user files cheaply. The cost of all this power is that you own more plumbing: you generate presigned URLs so browsers can upload directly and securely, configure CORS and bucket policies, put a CDN (CloudFront or Cloudflare) in front for fast global delivery, and build any image processing yourself (or add a service like Cloudinary or an image CDN on top). The practical guidance: choose S3/R2 when you want cheap, standard, portable object storage and are comfortable wiring up uploads and delivery — and strongly consider R2 specifically when egress bandwidth would otherwise make S3 expensive.

    How do signed URLs and upload security actually work, and why do they matter?

    The single most important security rule for file uploads is: never let the browser talk to your storage with long-lived credentials, and never route large files through your own server just to re-upload them. The pattern that solves both is the presigned (signed) URL. Your server, which holds the secret storage credentials, generates a short-lived, single-purpose URL that grants permission to upload one specific object (or download one specific file) for a few minutes; the browser then uploads the file directly to storage using that URL, and the credentials never leave the server or get exposed to the client. This keeps your keys secret, lets you enforce rules server-side before issuing the URL (is this user allowed to upload, is the file type and size acceptable, where should it be stored), and avoids your app server becoming a bottleneck for big files. With plain S3/R2 you implement this yourself with the AWS SDK's presigned-URL helpers plus CORS configuration on the bucket. UploadThing abstracts the whole dance away — your file router is exactly where you enforce the who/what/how-big rules, and the service handles the presigned upload and callback for you, which is a big part of its appeal. Cloudinary offers signed uploads and access-control features as well, so you're not exposing your account to anonymous writes. Beyond signed URLs, the same fundamentals apply across all three: validate file type and size on the server (not just the client), be cautious serving user-uploaded HTML/SVG, scan for malware if you accept arbitrary files, set sensible access controls (private by default for anything sensitive, public only where intended), and store the resulting file URL or key in your database rather than the file bytes themselves.

    Which is cheapest, and how should I think about cost?

    Cost splits along the same lines as the layers themselves, and the honest answer is it depends on your volume and your bandwidth. Raw object storage is the cheapest per gigabyte by a wide margin — S3 and its compatibles charge low storage rates plus request and egress fees — which is why storage-heavy apps gravitate to it. The trap with AWS specifically is egress: you pay for every gigabyte served out, so a media app with lots of downloads can see bandwidth, not storage, dominate the bill. That's the exact gap Cloudflare R2 targets, offering S3-compatible storage with no egress fees, which can make serving large volumes of user files dramatically cheaper; other alternatives like Backblaze B2 compete on price too. Cloudinary is priced as a media platform — a bundle of storage, transformations, and bandwidth (often expressed as credits) — so it's more expensive per unit than raw storage, but you're paying for the transformation pipeline and CDN you'd otherwise build and run yourself, which can be worth it for media-centric products and painful for high-traffic ones. UploadThing sits in between: it's a managed service priced above commodity storage because you're paying for the developer experience and the handled infrastructure, with a free tier and paid plans that are very reasonable at moderate scale but less cost-efficient than owning a bucket once you're moving serious volume. The way to think about it: for pure storage at scale, S3-compatible (especially R2 for egress-heavy workloads) wins on cost; for media transformation you'd otherwise build, Cloudinary can be worth its premium; for speed-to-ship at moderate volume, UploadThing's convenience is often worth paying for — and always model your real storage, request, and especially bandwidth numbers rather than trusting a headline price.

    Which file-storage choice should a SaaS or template you sell ship with?

    For a boilerplate, SaaS starter, or app you intend to hand off or sell, the guidance mirrors every other tooling decision: default to the option with the widest recognition, the cleanest integration, and the fewest strings attached for the buyer, and deviate only for a stated reason. Two defaults are defensible. If the template's selling point is fast, clean Next.js integration and a great out-of-the-box experience, UploadThing is an excellent ship — the integration is small, typed, and easy for a buyer to understand and re-point at their own account, which is exactly what you want a buyer to be able to do. If instead you want the most portable, cost-neutral, lock-in-free default, ship against the S3 API — ideally S3-compatible so the buyer can point it at AWS, Cloudflare R2, or any compatible host with only environment-variable changes; this signals a mainstream, low-risk choice and never forces the buyer onto a specific vendor's pricing. Cloudinary should ship only when the template is genuinely media-centric and the transformation pipeline is part of the value, and you should say so prominently, because a buyer expecting simple storage and inheriting a metered media bill is an unwelcome surprise. Whichever you choose, the resale rules are the same as for any code you sell: never hardcode storage credentials, access keys, or account IDs into the repo — put them in environment variables and ship an example env file; make the storage provider swappable behind a thin interface where practical so the buyer isn't married to your account; document exactly which service the app expects, how to create the equivalent bucket or app, and how to plug in their own keys; and provide sensible defaults for file-type and size limits plus signed-URL security so the buyer inherits a safe configuration, not an open write endpoint. A storage layer that's cleanly integrated, credential-free, and documented does as much to make a codebase read as production-ready as any feature you build on top of it.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →