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.
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
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):
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.
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:
// 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:
"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.jpgThat 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
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:
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):
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
| UploadThing | Cloudinary | AWS S3 (+ R2) | |
|---|---|---|---|
| **Layer** | Upload service (DX layer) | Media platform (storage + pipeline) | Raw object storage |
| **Best at** | Fast Next.js integration | Image/video transformation | Cheap, standard, portable storage |
| **Next.js DX** | Excellent — typed, prebuilt UI | Good SDKs + URL transforms | Manual (SDK + presigned URLs) |
| **Image processing** | Minimal | Best-in-class, on-the-fly | Build it yourself |
| **Cost model** | Managed plans (premium for DX) | Storage + transforms + bandwidth | Per-GB + requests + egress |
| **Cost at scale** | Higher | Can climb on high traffic | Cheapest (R2 kills egress fees) |
| **Lock-in** | Service-level | High (URL/transform syntax) | Low (S3 API is a standard) |
| **You own** | Almost nothing | Little (they run the pipeline) | Uploads, CDN, processing |
| **Ship it when** | Uploads today, moderate volume | Media is the product | You 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:
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
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:
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.
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.
