Presigned URLs: Secure Direct Uploads

The Upload Problem Nobody Talks About

When you hit publish on a photo post, you probably don't think about where that image goes. It just... works. But behind the scenes, there's a quiet architectural decision that decides whether your upload takes 2 seconds or 12 seconds.

The traditional approach is straightforward: client uploads to your API server, server processes the file, server uploads to cloud storage. Simple. Predictable. Completely wasteful.

Your API becomes a glorified pass-through. The request travels to your server, the server reads every byte into memory, processes it, then sends it back out to cloud storage. For a 5MB photo, that's 5MB up to your server, plus another 5MB from your server to storage. Your server is bottlenecked. Your user is waiting. Your infrastructure is bleeding cost.

At Jottings, I wanted uploads to be fast. Delightfully fast.

Why Proxying Uploads Is Broken

Let me paint a picture of what traditional file proxying looks like:

User Browser
    ↓ (5MB upload)
API Server (memory spikes, CPU busy)
    ↓ (5MB upload again)
Cloud Storage

Your API Lambda has a 15-minute timeout, but it's also memory-constrained. If you're on a tight memory budget, you might be copying the entire file into memory before uploading it elsewhere. That's wasteful. Even worse, if you have multiple concurrent uploads, your server gets hammered.

And here's the sneaky problem: your API is now on the hot path for every upload. If your server is slow or unreliable, every upload suffers. Your media storage reliability is now coupled to your API reliability.

From a cost perspective, it's brutal. You're paying for:

  • Your server to receive the bytes
  • Your server bandwidth to send bytes outbound
  • Storage transfer costs
  • The electricity and CPU cycles to shuffle bytes around

For a bootstrapped product, every dollar counts.

Enter Presigned URLs

Presigned URLs are one of those elegant solutions that feels obvious once you understand it. Instead of proxying files through your API, you give the client temporary permission to upload directly to your storage.

Here's how it works at Jottings:

  1. Client requests upload permission: POST /api/v1/media/upload-url
  2. Server validates and generates presigned URL: Ephemeral token with GET object path, expires in 15 minutes, single use only
  3. Server returns URL to client: Client gets a temporary, limited-scope upload token
  4. Client uploads directly to storage: Bypasses your API entirely
  5. Client confirms upload: Notifies API that media is ready (optional verification step)
User Browser
    ↓ (small request)
API Server (tiny!)
    ↓ (presigned URL response)
User Browser
    ↓ (5MB upload directly)
Cloud Storage

The API's work shrinks to milliseconds. No memory spikes. No bandwidth spent on file proxying.

The Security Question You're Thinking

"Wait, Vishal. Doesn't giving clients direct write access to storage open a security hole?"

Great question. This is why presigned URLs are brilliant.

Time-limited: The presigned URL expires after 15 minutes. After that, it's useless. A malicious actor can't use a leaked URL months later.

Scoped to one object: The URL works for one specific upload path only. Even if someone gets the URL, they can't use it to upload random files elsewhere in your storage bucket.

Single-use: More sophisticated implementations (like AWS S3's x-amz-algorithm and x-amz-credential) include request-signature validation. The signature includes the timestamp, the exact object key, and request headers. You can't reuse the URL with different content.

No API authentication required: The presigned URL itself is the credential. There's no JWT to intercept or session token to steal. The permission is embedded in the URL's cryptographic signature.

Validated on the backend: At Jottings, after the client uploads directly to R2, they notify the API: "I've uploaded media, here's the path." The API then verifies the file actually exists in R2 using a HEAD request before confirming the media record in the database.

So even if someone generates a fake presigned URL, they still can't trick the API into recording non-existent media.

Performance Benefits Are Real

Direct-to-storage uploads eliminate the middle man. For a photo on Jottings:

  • Latency: Drops from 8-12s (through API) to 2-4s (direct to CDN)
  • Server resources: Your API doesn't spend CPU or memory on file I/O
  • Concurrent uploads: Users don't compete for API bandwidth
  • Cost: You save on egress bandwidth from your API server
  • Reliability: Upload reliability is decoupled from API reliability

This matters more than you might think. When someone publishes a photo, they want instant feedback. Presigned URLs give us that.

Implementation Details

At Jottings, we use Cloudflare R2 (S3-compatible object storage):

// Generate presigned URL (15-minute expiry)
const url = s3.getSignedUrl('putObject', {
  Bucket: 'static-jottings',
  Key: `sites/${subdomain}.jottings.me/${timestamp}_${uuid}_${filename}`,
  Expires: 900, // 15 minutes
  ContentType: 'image/jpeg'
});

// Client uploads directly to this URL
await fetch(url, {
  method: 'PUT',
  body: fileBlob,
  headers: { 'Content-Type': 'image/jpeg' }
});

// API verifies the upload happened
const headResponse = await s3.headObject({
  Bucket: 'static-jottings',
  Key: uploadedKey
});

// Only if file exists, create media record in database
if (headResponse) {
  createMediaRecord(subdomain, uploadedKey);
}

The key insight: verification is cheap. A HEAD request costs almost nothing. The expensive part—uploading the file—happens directly.

Why This Matters for Jottings

Jottings is designed for simplicity. Simple design, simple maintenance, simple costs. Presigned URLs fit that philosophy perfectly:

  • Fewer moving parts: No file processing Lambda, no queue management
  • Simpler code: Client handles uploads, API handles validation
  • Better UX: Users experience faster uploads
  • Better financials: Lower bandwidth costs, less compute

The tradeoff is minimal. Your API has to trust the client to follow the rules, but the presigned URL mechanism makes those rules cryptographically enforced.

The Takeaway

If you're building a platform that handles user uploads, presigned URLs should be your default assumption, not an optimization you add later.

They're:

  • Secure (time-limited, scoped, signature-validated)
  • Fast (no API bottleneck)
  • Simple (standard cloud storage feature)
  • Cheap (save on bandwidth and compute)

The only real downside is they require a tiny bit more client-side logic. But in 2025, that's not a downside—it's the default.

If you're curious how Jottings handles uploads, you can check out our open architecture documentation. And if you're interested in blogging with a platform that respects your data and your bandwidth, well... you know where to find us.


Enjoyed this? Subscribe to get posts like this in your inbox, or follow along on your favorite platform.