←Back to blog
Article

Skipping the proxy on uploads with presigned S3 URLs

Skipping the proxy on uploads with presigned S3 URLs
S

StriveBit

4 min readCloud Solutions

Skipping the proxy on uploads with presigned S3 URLs

Take an education platform collecting video assignments from students. The files range from 40MB to 800MB. They are routed through a Node.js server, which reads the upload, streams it to S3, and returns the URL. During exam season, with a couple of hundred concurrent uploads, the server sits near its memory ceiling and is about to fall over.

The straightforward fix is to stop proxying the file through your application. Generate a presigned S3 URL, hand it to the client, and let the browser PUT directly to S3. Your server processes the request in milliseconds instead of streaming megabytes for minutes.

Generating the presigned URL

The server-side code is short. Using the AWS SDK for Node:

const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3 = new S3Client({ region: 'ap-south-1' });

async function generateUploadUrl(userId, fileName, contentType) {
  const key = `uploads/${userId}/${Date.now()}-${fileName}`;
  const command = new PutObjectCommand({
    Bucket: 'student-assignments',
    Key: key,
    ContentType: contentType,
  });
  return getSignedUrl(s3, command, { expiresIn: 300 });
}

The client takes that URL and does a PUT with the file as the body. No file touches your server.

What you give up

The tradeoff is real. When the server no longer sees the file, it cannot validate it. You cannot check the file size until the upload completes. You cannot scan for viruses before the file lands in your bucket. You cannot reject files based on content without downloading them back from S3 after the fact.

We handle this with constraints on the presigned URL and a post-upload step. The `getSignedUrl` call above pins the `ContentType`. To enforce a size limit, use S3's `Content-Length` condition during presigning — the upload fails if the client sends more bytes than the URL allows. For virus scanning, we configure an S3 event notification that triggers a Lambda function to scan the object and delete or quarantine it if needed. The file is briefly available in S3 unscanned, which is acceptable when the files sit in a private bucket with no public read access until an admin approves them. If your bucket serves files publicly the moment they land, that window matters and you need a quarantine bucket in between.

CORS, because S3 is a different origin

The browser will block the PUT without CORS configured on the bucket. Add a CORS policy that allows PUT methods from your application's origin, with the headers your client sends. This is a one-time setup step:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT"],
    "AllowedOrigins": ["https://app.example.com"],
    "MaxAgeSeconds": 3000
  }
]

Most teams hit this on day one, fix it, and forget it.

When not to do this

If your files are under 1MB and you already have validation logic in your upload handler, the proxy approach is fine. The overhead of generating presigned URLs, handling CORS, and building post-upload scanning is not worth it for small files. We use presigned URLs when files are large, uploads are concurrent, or the application server is the bottleneck. For a form collecting 200KB profile photos, we keep the simple multipart upload through the server.

Once uploads go direct, the Node server stops touching file content entirely. Memory usage during peak periods drops to a small fraction of what it was, because the server is no longer holding megabytes of video in flight. Upload completion time stays roughly the same for students, but the server can handle other requests while uploads are in progress, which is the difference between a platform that survives exam season and one that does not.

←Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch