deploy · tutorial · vps

500MB Uploads, 0 Server RAM: R2 Presigned

Big files without big servers: presigned R2 URLs upload direct from browser, zero RAM, zero SDK. The pattern inside.

Dockworkers rolling and hoisting heavy barrels of corn syrup onto a barge in New York, around 1912
Dockworkers rolling and hoisting heavy barrels of corn syrup onto a barge in New York, around 1912 · Photo: Lewis Hine, public domain, via Wikimedia Commons

The $5 box fell over on a 200MB video

Picture a user uploading a 200MB course video.

A handler that buffers the file holds all of it in memory before forwarding it to storage. On a 1GB box, a few of those at once can run it out of memory.

The app restarts. The upload dies near the end. The next attempt may take the database down with it.

The file is not the problem. The server is a needless middleman.

Remove it. Browsers talk to storage directly. Your server signs a permission slip.

The box never meets the bytes.

Proxied or signed

One large upload Proxied through your server Signed, direct
Server memory used The whole file, buffered Almost none
Server bandwidth The file in and out None
Risk on a small box Out of memory under load Fails alone, retried
SDK needed Server and client weight None, plain requests
Blast radius Can take the app down One upload

Proxying uploads is worse than slow. Three reasons.

Buffers scale with users, not code. One upload fits. Three at once can overwhelm a small box. Direct uploads put the concurrency on the storage provider.

Traffic is counted twice. Proxied bytes cross your server's bandwidth in and out. A direct upload crosses once, and Cloudflare R2 does not charge for download traffic.

Failures cascade through shared memory. Heavy uploads can starve the database and the app at once. Isolated uploads fail alone.

What to do

Ask the server for a signed upload address.

Your signed-in user asks for permission to upload one file. The server checks who they are, what kind of file it is and how big. Only then does it hand back a short-lived address.

Permission is checked where the address is minted, because storage trusts signatures, not sessions. A leaked address expires quickly.

Send the file from the browser.

A plain request straight to storage. No storage SDK in your app. No storage credentials in the browser, only an address that expires.

The same code handles a 5KB avatar and a large video.

Finalize, then serve.

After the upload, the app calls a finalize step. The server confirms the file exists, with the expected size and type, before it records anything.

Serve files through your own domain, so you are never tied to one vendor's address. Downloads and deletes use the same signed approach.

Why signing is safer than it looks

The server decides the file's name and location, and ignores the original filename. That closes off path traversal tricks.

Each person can only touch files under their own folder.

Allowed types, folders and size limits live in the kit's storage settings. Anything outside the list is rejected before an upload address exists.

Works with R2 and S3

Pick the provider with one setting.

Pick R2 if your files are downloaded often, because download traffic is where object storage bills grow. S3 charges for it. Check each provider's current pricing.

Test it

Upload a small file and a large one. Open both from your own domain.

Try a file type that is not on the allowed list. Confirm that it is rejected before any upload begins.

Then try to fetch another user's file. It should be refused. Five minutes, and you know the walls hold.

Know this first

One signed upload per file. No chunked resume for huge files. No virus scanning.

Add those if you need them.

Why BoringPush is the way to do this

Uploads that never touch your server.

The browser sends the file straight to storage. Your server signs and checks. Memory stays flat.

Works with R2 or S3. One setting chooses.

FAQ

Does this work with plain S3 as well as R2?

Yes. The signing approach is the same for both.

You choose the provider with a setting. The difference is the bill: S3 charges for download traffic and R2 does not.

How are permissions enforced without the server in the middle?

At signing time, the server checks who is asking, the file type and the size. The address expires quickly, and the finalize step re-checks the file before anything is recorded. Permission comes before the bytes, and verification comes after them.

What about virus scanning?

BoringPush does not scan files for you. If you add scanning, do it after the upload, in the background.

Inline scanning blocks uploads and burns server CPU, which is the memory problem in antivirus clothing. Hold files in a pending state until they are clear.

Can downloads use the same pattern?

Yes. Private files get short-lived signed download addresses.

Public assets such as avatars can be served from a public address. Private by default with signed exceptions beats public by default with regrets.

What happens on a dropped phone connection?

A single signed upload restarts from the beginning. For large files on bad networks, you would add chunked uploads on top.

When should uploads go through the server after all?

For tiny trusted files such as small avatars, where simplicity wins, and when the server must transform the file on the way in. Proxy the small and simple, and sign everything else.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading