S3 vs GCS vs Azure Blob for Media Uploads

For direct browser uploads of media, Amazon S3, Google Cloud Storage, and Azure Blob are functionally interchangeable; the real differences are in the signed-upload model, how chunked uploads work, and what egress costs you. Pick by your existing cloud and your egress profile, not by raw feature lists.

This comparison sits within Direct-to-Cloud Upload Patterns under Backend Validation & Cloud Storage Architecture, and assumes you already plan to upload from the browser straight to storage.

When to use this comparison

  • You are starting a media pipeline and have not committed to a cloud yet.
  • You run multi-cloud and need to know where the upload code differs.
  • You are estimating egress cost for serving media back to users.

The decision at a glance

Dimension Amazon S3 Google Cloud Storage Azure Blob
Signed single PUT getSignedUrl + PutObjectCommand file.getSignedUrl({ action: "write" }) SAS token on BlockBlobClient
Browser POST form POST policy via createPresignedPost generateSignedPostPolicyV4 SAS query params on container
Chunked model Multipart upload (UploadPart, ETags) Resumable upload (single session URI) Block blob (stageBlock + commitBlockList)
Chunk identity Per-part ETag returned Byte-range offsets, no per-chunk ID Base64 block IDs you choose
CORS scope Per-bucket JSON rules Per-bucket JSON rules Per-account (Blob service) rules
Expose ETag for chunks Required (ExposeHeaders) Not needed (offset-based) Required for verification
Egress to internet Tiered per-GB, free to CloudFront Tiered per-GB, free to Cloud CDN Tiered per-GB, free to Azure CDN/Front Door
Primary SDK @aws-sdk/client-s3 (modular v3) @google-cloud/storage @azure/storage-blob
Free request tier Generous PUT/GET tiers Generous tiers Generous tiers

Signed upload model

S3 signs a URL or a POST policy. A presigned PUT is the simplest: you sign a single URL and the browser sends the file with a matching Content-Type. The exact generation is in generating secure presigned URLs with AWS SDK v3.

GCS offers the same V4 signed URL for a single write, plus generateSignedPostPolicyV4 for a form POST that can enforce content-length and prefix constraints. The full flow is in uploading to GCS with the Node.js client libraries.

Azure uses a Shared Access Signature (SAS) β€” a signed query string appended to the blob URL that grants scoped, time-bound write permission. See uploading to Azure Blob with the storage JS SDK.

// S3: one signed PUT URL the browser can use directly.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: process.env.AWS_REGION });

export function signS3Put(bucket: string, key: string, contentType: string) {
  return getSignedUrl(
    s3,
    new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType }),
    { expiresIn: 900 },
  );
}

Chunked / large-file model

The three clouds diverge most here, and it dictates your frontend resumable upload state machine.

Chunked upload models compared S3 uploads parallel parts identified by ETags, GCS streams byte ranges to one resumable URI, Azure stages blocks by ID then commits a list. S3 multipart part 1 part 2 parallel + ETags complete with ordered part list GCS resumable session URI sequential byte ranges to one URI resume from offset Azure blocks block A block B stage by chosen ID commitBlockList to assemble
Three chunking models: S3 parts with ETags, GCS byte ranges to one URI, Azure staged blocks committed by list.
  • S3 multipart splits the file into parts, uploads each independently (great for parallelism), and finalizes by sending the ordered list of part numbers and their ETags. You must expose ETag via CORS for the browser to read them.
  • GCS resumable opens one session URI and the client PUTs sequential byte ranges to it, resuming from the last acknowledged offset after an interruption. There is no per-chunk identifier β€” the server tracks the offset.
  • Azure block blobs let you stageBlock chunks under base64 block IDs you assign, in any order, then commitBlockList to assemble them. This gives S3-like parallelism with explicit, client-chosen IDs.
// GCS: a resumable session URI is the single endpoint for all chunks.
import { Storage } from "@google-cloud/storage";

const storage = new Storage();

export async function gcsResumableUrl(bucket: string, name: string) {
  const [uri] = await storage
    .bucket(bucket)
    .file(name)
    .createResumableUpload({ metadata: { contentType: "video/mp4" } });
  return uri; // Client PUTs byte ranges to this single URI.
}

CORS differences

S3 and GCS apply CORS per bucket; Azure applies it to the whole Blob service of the storage account. All three need PUT/POST allowed and, for chunked uploads that read part identifiers, ETag exposed. The full cross-provider setup is in CORS configuration for uploads. The practical consequence: on Azure you cannot scope CORS to one container, so plan account boundaries accordingly.

Pricing and egress

Storage-at-rest pricing is close enough across the three that it rarely decides anything. Egress dominates media workloads, because you serve the files back far more than you store them. All three charge tiered per-GB for internet egress and waive it (or steeply discount it) when you serve through their own CDN β€” CloudFront for S3, Cloud CDN for GCS, Azure CDN or Front Door for Blob. If you serve media at scale, budget for the CDN path from day one; raw bucket egress is the expensive way to deliver media. Inbound uploads are free on all three, so the upload side is not a cost factor.

SDK ergonomics

  • @aws-sdk/client-s3 is modular and tree-shakable; you import only the commands you use, and presigning is a separate @aws-sdk/s3-request-presigner package. Most third-party uploader libraries target S3 first.
  • @google-cloud/storage is a single high-level package with an object-oriented bucket().file() API; resumable uploads and signed URLs are one method call each. The least boilerplate of the three.
  • @azure/storage-blob is explicit and verbose β€” separate clients for service, container, and blob β€” but the block model maps cleanly onto custom chunked uploaders.

Recommendation

  • Already on AWS, or want the widest ecosystem of uploader tooling: choose S3. Multipart is battle-tested and almost every client library supports it.
  • Already on Google Cloud, or you want the simplest resumable large-file flow: choose GCS. The single session URI is the easiest resumable model to reason about.
  • Already on Azure, or you need client-controlled out-of-order chunk assembly: choose Azure Blob. Block IDs give you the most control over chunk handling.

In short, let your existing cloud decide unless egress economics or a specific chunking need pulls you elsewhere. The upload code is similar enough that switching later is a contained refactor, not a rewrite.

FAQ

Which is cheapest for a media upload service?

At-rest costs are comparable; egress is what differs, and all three are cheapest when you serve through their native CDN. Estimate your serve-to-store ratio and price the CDN path, not bucket egress.

Can I use the same frontend uploader for all three?

For single signed PUT uploads, yes β€” it is just a fetch PUT to a URL. For chunked uploads the models differ (multipart ETags vs resumable offsets vs block IDs), so the chunking logic must branch per provider.

Does Azure’s account-wide CORS matter in practice?

It matters if you host multiple apps with different origin needs in one storage account, since they share one CORS policy. Separate storage accounts give you isolation that S3 and GCS get per bucket.