Presigned URL vs Server Proxy Tradeoffs

A presigned PUT sends the file straight from the browser to object storage, sparing your API server the bandwidth and memory; a server proxy routes the file through your API first, giving you a synchronous chokepoint to validate before anything lands in storage. Choose presigned for scale and cost, proxy for inline control.

This decision guide sits within S3 Presigned URL Workflows under Backend Validation & Cloud Storage Architecture. It complements the broader direct-to-cloud upload patterns discussion.

When to use this comparison

  • You are deciding how files should reach storage for a new upload feature.
  • Your API servers are saturating on upload bandwidth or memory.
  • You need to enforce validation or transformation before a file is accepted.

The two paths side by side

Factor Direct presigned PUT Server proxy
File data path Browser β†’ storage Browser β†’ API β†’ storage
API bandwidth used Zero for the file body Full file size, twice (in + out)
API memory/CPU Negligible Buffers or streams every byte
Latency One hop to storage Two hops, serialized
Validation point After upload (async) Before upload (synchronous)
Credential exposure Short-lived signed URL None reaches the client
Horizontal scale cost Storage scales itself API must scale with upload volume
CORS required Yes (browser cross-origin) No (server-to-server)
Best for Large media, high volume Small files needing inline checks

Latency

A presigned PUT is a single network hop: the browser opens one connection to the storage endpoint and streams the file there. A proxy is two serialized hops β€” the browser uploads to your API, then your API uploads to storage β€” so the file effectively travels its own size twice before it is durable. For large media the proxy path roughly doubles wall-clock upload time and ties up an API connection for the entire duration.

Presigned direct path versus proxy path Top row: browser uploads directly to storage in one hop. Bottom row: browser uploads to the API which forwards to storage in two hops. Presigned: one hop Browser file body (1x) Storage Proxy: two hops Browser body (1x) API validate here body (2x) Storage Proxy moves the file body through the API; presigned never touches it.
The presigned path is a single hop to storage; the proxy path moves the file body through the API server twice.

Cost

With presigned uploads the file body never passes through your compute, so you pay only for storage and the negligible cost of signing a URL. With a proxy you pay for the API instance time spent moving bytes, plus the data-transfer cost of receiving and re-sending the file β€” and you must provision enough API capacity to absorb peak upload concurrency. At any meaningful media volume the proxy is the markedly more expensive architecture, which is the core argument made in direct S3 uploads vs proxy uploads performance.

Validation point

This is the proxy’s one decisive advantage. Because every byte passes through your code, you can validate the file signature, enforce size, scan content, or reject it before it is ever stored β€” synchronously, returning a clean error to the user. The presigned path stores the file first and validates after the fact, so you need an asynchronous pipeline: an S3 event triggers a check, and a bad file is quarantined or deleted. That pattern is covered in serverless virus scanning with AWS Lambda and validating file signatures with libmagic.

Security

A proxy never exposes storage credentials to the client at all β€” the API holds them and the browser only ever talks to your domain. A presigned URL deliberately hands the client a scoped, time-bound credential; security then rests on signing it narrowly (single key, write-only, short expiry) and locking the content type. Both are secure when done right, but the presigned model has a larger surface to get wrong, which is why generating secure presigned URLs emphasizes least-privilege signing.

A hybrid: presign, then validate asynchronously

Most production media systems pick presigned for the transport and recover the proxy’s validation guarantee asynchronously. The browser uploads directly with a signed URL into a quarantine prefix; an upload event triggers signature and virus checks; only on success does the object move to its serving prefix.

import { S3Client, CopyObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";

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

// Runs from an S3 ObjectCreated event on the quarantine prefix.
export async function promoteIfClean(
  bucket: string,
  key: string,
  isClean: boolean,
): Promise<void> {
  if (!isClean) {
    await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
    return;
  }
  const target = key.replace(/^quarantine\//, "ready/");
  await s3.send(
    new CopyObjectCommand({
      Bucket: bucket,
      CopySource: `${bucket}/${key}`,
      Key: target,
    }),
  );
  await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
}

This keeps the cost and latency profile of presigned uploads while ensuring nothing reaches the serving path unvalidated.

Configuration gotchas

Choosing proxy and then hitting memory limits

A proxy that buffers the whole file in memory crashes on large uploads. If you proxy, stream the request body straight to storage rather than buffering it, or the API will OOM under big files.

Choosing presigned and forgetting async validation

A presigned URL stores whatever the client sends. Without an upload-triggered validation step, malicious or malformed files sit in your bucket. Always pair presigned uploads with an asynchronous check.

Verification

Confirm the file path you intended is the one in use. For presigned, check that the upload request in DevTools goes to the storage host, not your API:

# Presigned: the request URL host should be the storage endpoint, not your API.
curl -i -X PUT "https://my-bucket.s3.amazonaws.com/uploads/test.bin?X-Amz-Signature=..." \
  -H "Content-Type: application/octet-stream" \
  --data-binary @test.bin
# Expected: HTTP/1.1 200 OK directly from S3, with no API hop.

FAQ

Is a presigned upload less secure than a proxy?

Not inherently, but it has more to configure correctly. The credential reaches the client, so it must be scoped tightly and short-lived. A proxy keeps credentials server-side at the cost of bandwidth and latency.

Can I validate file content with presigned uploads?

Yes, but asynchronously. Upload into a quarantine prefix, validate on the storage event, and promote only clean files. You cannot block a bad upload synchronously the way a proxy can.

When is a proxy actually the right choice?

For small files where inline validation, transformation, or a strict synchronous accept/reject matters more than bandwidth β€” for example, parsing and rewriting a small document before storing it.