If your product lets users upload anything — profile photos, invoices, CSVs, video, design files — the upload button is one of the most deceptively risky features you’ll ship. It looks like a form field. In practice it’s a system design problem that touches your servers, your storage bill, your security posture and your background job queue. This guide is for founders and CTOs who are past “just use a file input” and want a file upload architecture that holds up once real users, and real file sizes, show up.
We’ll cover the pattern almost every scalable product uses, the security decisions you can’t skip, and where off-the-shelf tools make more sense than building it yourself.
Why file uploads break simple architectures
The naive approach — the browser sends the file straight to your application server, which writes it to disk or forwards it to storage — works fine in a demo and badly in production. Every upload now occupies a request thread or worker for as long as the transfer takes. A handful of users on slow connections uploading 200MB videos can quietly exhaust your server’s memory and connection pool while everyone else’s requests queue behind them.
It also couples your API’s availability to file size: a timeout tuned for JSON payloads will kill a genuine upload, and a timeout generous enough for uploads leaves you exposed to slow-drip abuse. None of this shows up in a Figma mockup or an MVP demo, which is exactly why it gets underestimated.
The file upload architecture that scales: direct-to-storage uploads
The fix is to stop routing file bytes through your application server at all. Instead:
- The client asks your API for permission to upload, sending metadata only — filename, content type, size, maybe a checksum.
- Your backend validates that request, decides where the file should live, and returns a short-lived presigned URL (or equivalent signed upload token) for object storage such as Amazon S3, Google Cloud Storage or Cloudflare R2.
- The browser or app uploads the bytes directly to that storage bucket — your server never sees the payload.
- The client confirms completion with your API, which flips a database record from “pending” to “ready” once storage confirms the object exists.
This keeps your application servers doing what they’re good at — authorisation and bookkeeping — and lets storage providers, which are built for exactly this, handle the data transfer. Your server’s memory and request timeouts stop scaling with file size, which matters enormously once you have more than a handful of concurrent uploads.
Multipart uploads for anything large
For files above roughly 100MB, a single PUT request becomes fragile — one dropped connection and the user starts again from zero. Multipart upload splits the file into chunks, each uploaded (and retried) independently, then reassembled by the storage service once all parts land. Most teams get good results uploading 3–5 parts in parallel; more than that tends to hit diminishing returns or network contention on typical connections.
If your users are on genuinely unreliable networks — field teams, mobile uploads, large media files — look at the tus protocol or similar resumable-upload approaches. Multipart upload survives a dropped connection between parts; a true resumable protocol survives it mid-part and picks up from the last confirmed byte, which is a meaningfully better experience for anyone uploading a gigabyte-plus file on patchy wifi.
Security: the part teams skip and later regret
A file upload endpoint is an entry point for arbitrary data onto your infrastructure, so it deserves the same scrutiny as a login form. A few non-negotiables:
- Never trust the filename. User-supplied filenames can carry path traversal sequences, odd encodings, or simply collide with another user’s file. Generate your own storage key — something like
{tenantId}/{uuid}— and keep the original filename as metadata, not as part of the path. - Validate content type and size server-side, before you issue the presigned URL, not after the fact. The client’s declared MIME type is a claim, not a fact — confirm it against the actual file where it matters (e.g. checking magic bytes for images).
- Keep presigned URLs short-lived — five to fifteen minutes is typical — so a leaked link doesn’t become a standing hole in your bucket.
- Scope the storage key prefix server-side so a client can only ever write into their own tenant’s or user’s folder, never an arbitrary key.
- Scan what you accept. If uploads are visible to other users — shared documents, public avatars, marketplace listings — run them through malware scanning (ClamAV is the common open-source choice) before they’re served onward.
This overlaps heavily with the broader checklist in our web app security checklist for founders, which is worth running alongside any feature that accepts user-supplied data.
Process uploads asynchronously, not inline
Once a file lands in storage, there’s usually follow-up work: generating thumbnails, extracting text from a PDF, transcoding video, running the virus scan mentioned above, or updating a search index. None of that should happen inside the request that confirms the upload — it should be handed to a background job queue that can retry on failure and scale independently of your web tier.
This is the same architecture decision we cover in background job queues: when startups actually need one — file processing is one of the clearest cases where “just do it synchronously” stops working the moment files get larger or more numerous than your prototype assumed.
Protect the upload endpoint like any other write path
An endpoint that issues presigned URLs is still an API endpoint, and it’s an appealing target for abuse — a script requesting thousands of upload tokens a minute, or a user trying to exhaust your storage quota. Apply the same throttling discipline you’d apply anywhere else: per-user and per-IP rate limits on the token-issuing endpoint, sane maximum file sizes enforced both client- and server-side, and quotas tied to plan tier if you’re running a paid product. Our guide to API rate limiting best practices covers the mechanics in more depth.
Delivery and cost: don’t forget the other end
Getting files in is half the problem; serving them back out efficiently is the other half. Put a CDN in front of anything downloaded repeatedly — avatars, product images, public documents — rather than serving directly from storage on every request. This is standard territory covered in our caching strategy for web apps guide, and it meaningfully reduces both latency and storage egress costs.
Storage itself is cheap until volume grows, at which point lifecycle rules matter: move rarely-accessed files to cheaper storage tiers automatically, and set retention or deletion policies for anything that doesn’t need to live forever. If uploads are a significant part of your infrastructure spend, it’s worth reading alongside our broader cloud cost optimisation guide.
Build it or buy it?
None of this means every product needs a bespoke upload pipeline from day one. Services like Uploadcare, Cloudinary and Transloadit bundle presigned uploads, transformation and CDN delivery behind a single API, and they’re a sensible choice if file handling is incidental to your product rather than core to it. Build your own when files are central to what you’re selling — a document platform, a media tool, a marketplace with heavy image use — because that’s where the control over cost, retention and processing pays for itself. Either way, the underlying pattern (direct-to-storage, signed, validated, processed asynchronously) stays the same; only who operates it changes.
If you’re scoping a feature that depends on file uploads and want a second opinion on the architecture before you build it, book a free consultation and we’ll talk through what actually fits your product and scale.
FAQ
Do I need presigned URLs for a small app with light upload volume?
Even at low volume, routing uploads through your server ties up request capacity and complicates timeout handling. Presigned uploads are cheap to implement early and avoid a rebuild later, so it’s rarely premature.
What file size should trigger multipart upload?
Most storage providers recommend multipart for files above roughly 100MB, though the exact threshold depends on your users’ typical connection quality, with flakier networks benefiting from multipart or resumable upload at smaller sizes too.
Is it safe to let users upload any file type?
No. Validate content type server-side, restrict to an allow-list relevant to your product, and scan anything that will be shared with other users or served publicly.
Should uploaded files go straight into my main database?
No. Store the files in object storage and keep only metadata, such as key, owner, status, size and checksum, in your database. Databases handle rows well and large binary blobs poorly.