How should I handle file uploads on serverless — presigned URLs, streaming, or multipart?
Handling file uploads on serverless platforms where request body size limits (Vercel 4.5MB, Lambda 6MB sync, Workers 100MB) force different architectural patterns depending on file size and platform
Blockers
- requires_version: capability/presigned-url-direct-upload → package/amazon-s3
- S3-compatible presigned URLs
- XML API POST Object V4
- requires_version: capability/multipart-upload-with-chunking → package/amazon-s3
- S3-compatible multipart API; 5MB minimum except last part
- UploadThing integrates directly with Next.js; Next.js-focused
- @aws-sdk/lib-storage is a Node.js library for client-side chunking
- breaking_change_in: runtime/aws-lambda → n/a
- breaking_change_in: runtime/aws-lambda → n/a
- Vendor lock-in for media URLs
- Migration requires re-pointing stored media URLs
- Migration requires re-pointing stored media URLs
Who this is for
- serverless
- cost-sensitive
- small-team
Candidates
Presigned URL Direct Upload (S3/R2/GCS)
Server generates a presigned upload URL; client uploads directly to object storage, bypassing the serverless function entirely. Works with any file size. S3 presigned URLs support up to 5GB single upload, 5TB multipart.
When to choose
When files exceed your platform's request body limit (Vercel 4.5MB, Lambda 6MB). Best for serverless + cost-sensitive constraints — the upload never touches your function, so no compute cost for transfer. The standard pattern for production file uploads on serverless.
Tradeoffs
No function compute cost for upload transfer. Works with any file size. Client uploads directly — reduces latency by eliminating the server relay. Requires CORS configuration on the storage bucket. Server-side validation must happen post-upload (via S3 event trigger or polling). Cannot validate file content before it's stored.
Cautions
Presigned URLs have expiration times (default 15 min, max 7 days for S3). Set Content-Type and Content-Length-Range conditions on the presigned URL to prevent abuse. Always validate uploaded files after storage — check file type, size, and scan for malware. Use S3 lifecycle rules to auto-delete uploads that fail validation.
Server-Side Relay (small files only)
Upload through the serverless function body. Limited by platform: Vercel 4.5MB (all plans), Lambda 6MB sync payload, Cloudflare Workers 100MB (Free/Pro), 200MB (Business), 500MB (Enterprise).
When to choose
When files are small enough to fit within platform limits and you need server-side validation before storage. Best for small-team + serverless where avatar uploads, CSV imports, or document uploads under 5MB need immediate validation (file type, content, virus scan) before acceptance.
Tradeoffs
Simplest implementation — standard multipart form handling. Server validates before storing. Function execution time and memory consumed during upload. Platform size limits are hard ceilings that cannot be increased. At Vercel's 4.5MB limit, even moderate image uploads may fail.
Cautions
Vercel's 4.5MB limit applies to the entire request body including headers and form fields — actual file capacity is slightly less. Lambda's 6MB limit is for synchronous invocation; async invocation payload is 1MB (increased from 256KB in October 2025). API Gateway has a separate 10MB request body limit. Lambda response streaming supports up to 200MB (increased from 20MB in July 2025). Always implement presigned URL fallback for files near the size limit.
Multipart Upload with Chunking (large files)
Split large files into chunks on the client, upload each chunk via presigned URLs, then complete the multipart upload. S3 supports 10,000 parts of 5MB-5GB each (up to 5TB total). R2 and GCS have similar multipart APIs.
When to choose
When uploading files larger than 100MB or when upload reliability matters (resume on failure). Best for cost-sensitive + serverless where video, backup, or large dataset uploads need resilient transfer without server-side infrastructure.
Tradeoffs
Resumable — failed chunks can be retried without restarting the entire upload. Parallel chunk upload for faster throughput. More complex client implementation (track upload parts, handle completion). Incomplete multipart uploads incur storage costs until aborted — configure lifecycle rules.
Cautions
S3 charges for incomplete multipart uploads until explicitly aborted. Set AbortIncompleteMultipartUpload lifecycle rules (e.g., 7 days). Libraries like @aws-sdk/lib-storage (Node.js), Uppy, or tus-js-client handle chunking client-side. For R2, the multipart API is S3-compatible but verify part size minimums (R2 requires 5MB minimum except last part).
Upload Service (UploadThing, Transloadit, Cloudinary)
Managed upload infrastructure handling storage, processing, and CDN delivery. UploadThing: free 2GB, $10/month for 100GB. Cloudinary: free 25 credits/month (credits are fungible across bandwidth, storage, and transformations). Transloadit: $69/month Startup plan (40GB included).
When to choose
When you need image/video processing (resize, transcode, optimize) in addition to upload, and building it yourself exceeds small-team capacity. Best for small-team + cost-sensitive where the total cost of presigned URLs + processing Lambda + CDN exceeds a managed service.
Tradeoffs
Handles upload, processing, and delivery — one vendor for the entire media pipeline. UploadThing integrates directly with Next.js. Cloudinary's free tier (25 credits) covers small apps but credits are shared across bandwidth, storage, and transformations — not dedicated bandwidth. Vendor lock-in for media URLs. Processing costs can spike with video transcoding. Migration requires re-pointing all stored media URLs.
Cautions
UploadThing is Next.js-focused — evaluate framework compatibility for other stacks. Cloudinary uses a credit-based pricing model where 1 credit = 1GB bandwidth OR 1GB storage OR 1,000 transformations — realistic usage splits reduce effective bandwidth well below the headline number. Transloadit charges per encoding minute ($69/month Startup includes 40GB, $1.80/GB additional). Always have an exit strategy — store original files in your own S3/R2 bucket even when using a processing service.
Try with your AI agent
$ npm install -g pocketlantern $ pocketlantern init # Restart Claude Code, Cursor, or your MCP client, then ask: # "How should I handle file uploads on serverless — presigned URLs, streaming, or multipart?"