Direct-to-cloud uploads
If the file's final home is S3, Azure Blob or GCS, sending it through your web server is usually wasted work. Direct-to-cloud lets the browser talk to storage, while your server keeps control of who may write what.
Why not just proxy it?
Proxying works, and for small files it is perfectly fine. It stops being fine when files get big:
- Your bandwidth is paid twice — once inbound from the user, once outbound to storage.
- Long requests occupy app resources that could be serving pages.
- You inherit the limits — body size caps, proxy timeouts, disk for temp files.
- Scaling is awkward — a resumable upload pinned to one instance's temp disk fights your load balancer.
Direct-to-cloud removes all four: the bytes never touch your app.
How it works: the browser never gets your keys
This is the part worth being precise about. Your cloud credentials stay on the server. The browser only ever receives a short-lived, narrowly-scoped URL that permits one specific write.
1. Browser -> Your server : "I want to upload invoice.pdf (12 MB)"
2. Your server : authenticate, authorise, pick the key
3. Your server -> Browser : pre-signed PUT URLs (expire in minutes)
4. Browser -> S3/Azure/GCS: PUT the parts directly
5. Browser -> Your server : "done" -> server finalises + records it Steps 2 and 5 are where your rules live: who may upload, what it may be called, size and type limits, and what row gets written in your database.
The three providers
| Provider | Mechanism | Worth knowing |
|---|---|---|
| Amazon S3 | Multipart upload with a pre-signed PUT per part, then CompleteMultipartUpload. | Parts must be 5 MB+ (except the last). Unfinished uploads keep costing money — set a lifecycle rule to abort them. |
| Azure Blob | PutBlock for each block, then PutBlockList to commit. | Uses a SAS URL. Uncommitted blocks are invisible until the block list is committed. |
| Google Cloud Storage | A resumable session URL; PUT ranges against it. | Chunks should be a multiple of 256 KiB. Query the session to learn the committed offset. |
The gotcha that costs an afternoon: CORS
The browser is now making cross-origin requests to storage, so the bucket — not your app — must allow it. Symptoms are opaque network errors with no useful status code.
Allow your site's origin, the methods you use (PUT, and POST where relevant), the headers you send, and — the one people miss — expose ETag. S3 multipart completion needs the ETag of each part, and if it is not in the exposed headers the browser hides it and completion fails.
Direct-to-cloud in ASP.NET Core
CoreUpload keeps the signing on your server behind an interface you implement, so no SDK credentials reach the browser:
// Program.cs - your signer owns the credentials
builder.Services.AddSingleton<IS3Signer, MyS3Signer>();
app.MapCoreUploadEndpoints(); // maps /s3/create, /s3/sign, /s3/complete... <core-upload asp-upload-url="/api/upload"
asp-strategy="s3"
asp-chunk-size="8MB"
asp-chunk-concurrency="4"></core-upload>