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.

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
- Cloudflare Tunnel + Next.js: Hackers Find Zero Open Ports — the tunnel uploads travel beside
- Docker Compose + Next.js: Don't Let It Eat Your SQLite — volumes surviving upload traffic
- Fix Production From Your Phone: The Grocery-Line Drill — midnight upload incidents, handled
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


