Taryana
Back to All Technical Articles/Direct Upload Architecture to Cloudflare R2 with S3 Presigned URLs, Go, and Next.js in Multi-Tenant SaaS
GoCloudflare R2S3 Presigned URLNext.jsSaaSArchitecture

Direct Upload Architecture to Cloudflare R2 with S3 Presigned URLs, Go, and Next.js in Multi-Tenant SaaS

Lessons from real production war stories handling server OOM crashes, a 1.8 TB quota leak, and egress bills: how direct Cloudflare R2 presigned uploads with Go and Next.js achieved 99.5% memory efficiency.

Taryana•2026-09-20•15 min read

1. Why Funneling File Uploads Through Application Servers Inevitably Cripples Your Backend

Have you ever experienced that nightmare scenario where your backend server process suddenly suffers an out-of-memory crash (Linux Kernel ) right as several users upload video assets or multi-megabyte PDF files simultaneously? Or opened your monthly cloud hosting bill only to be blindsided by hundreds of dollars in unexpected bandwidth charges?

That exact nerve-wracking ordeal is what my engineering team and I encountered when we first deployed our collaborative document management SaaS platform. Early on, we adopted the most intuitive, common approach:

TEXT Reference1 baris kode
> [Client Browser] ──(Multipart Form-Data)──> [Backend Server] ──(Stream SDK)──> [S3 Storage] ...Klik untuk melihat implementasi lengkap →

On paper, our controller code looked simple, and manual tests with 2 MB files on developer laptops passed without a hitch. But the moment the platform was released to enterprise customers uploading files ranging from 50 MB to 500 MB, the traditional multipart proxy architecture collapsed:

  • Cascading Server Crashes (Out-Of-Memory Panic): When dozens of enterprise users uploaded large archives over saturated office networks, backend worker threads held raw binary chunks in RAM buffers. Server memory surged from 150 MB to 7.8 GB in under 45 seconds, triggering the Linux and plunging our into continuous restart loops.
  • Slow Client Network Starvation: Users on sluggish cellular networks tied up HTTP connection sockets for minutes just to stream one file. As a result, other users attempting simple logins or record updates were locked out with 502 Bad Gateway errors.
  • Dual Invoicing: Our backend paid double for bandwidth: when bytes entered our API, and when our server pushed those same bytes into object storage.
  • Catastrophic Tenant Quota Leakage (Storage Double-Spend): Because storage counters were only updated after files finished uploading, a tenant opened 5 browser tabs simultaneously to upload large archives, accumulating 1.8 TB of storage on a subscription plan capped at 250 GB!

From this series of painful failures, we established an inviolable architectural rule: large binary data streams must never touch application server process memory. The backend server acts strictly as an Identity & Quota Gatekeeper, while raw binary streams flow directly from the client's browser to object storage using S3 .

Executive Summary & Core Insight

Solution Blueprint: Direct Cloudflare R2 Uploads with Two-Phase To permanently eradicate server panics and eliminate fees, we engineered a 3-pillar pipeline:

  1. 1Go Quota Gatekeeper & Presign Generator: The client transmits only lightweight metadata (filename, byte size, MIME type). The Go backend atomically verifies the subscription quota in PostgreSQL, acquires a temporary soft reservation, and issues a cryptographically signed R2 URL with a strict 15-minute expiration window.
  2. 2Direct Browser Streaming to Cloudflare R2: The Next.js frontend streams binary bytes directly to Cloudflare R2 via HTTP PUT with real-time transfer telemetry. Cloudflare R2 waives all outbound data transfer fees ($0 Policy), radically slashing cloud storage bills.
  3. 3 Confirmation Callback (): Once R2 accepts the file, the backend confirms actual object dimensions via HeadObject, atomically converting the soft reservation into a permanent storage commitment (hard commit).
Topologi Arsitektur: Direct Upload Cloudflare R2 dengan Presigned URL & Quota EngineDirect Storage & SaaS Quota
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
CLIENT (Next.js App)Selected: invoice_archive.zipSize: 45.8 MB (MIME: zip)1. Request Presign JSON2. Receive R2 Signed PUT URL3. Direct Streaming PUT (XHR)4. Confirm & Commit RPCZero Server Proxy OverheadBACKEND (Go API Gateway)Quota Enforcement EngineALLOW: Soft Reserve + TicketAWS SDK v2 S3 PresignerSigV4 + Expiry: 15m + Length LockFinalizer: S3 HeadObjectVerify Physical Bytes & Commit DBSTORAGE (Cloudflare R2)Global Anycast IngestionDirect Ingress & Zero-EgressSigV4 Cryptographic GuardPASS: Content-Length verifiedTenant Isolated Buckettenants/{id}/uploads/{uuid}1. POST /presigned-url2. Signed R2 URL + TicketDIRECT BINARY STREAM (HTTP PUT)HeadObject (Verify)4. POST /confirm-uploadARCHITECTURAL OUTCOME:Zero Server RAM Spike (Under 35 MB)Zero Egress Fee ($0 Data Transfer Out)Zero Quota Leakage (Atomic Handshake)
Normal Flow: Next.js -> Go API (Soft Quota Reserve) -> R2 Presigned URL -> Direct Browser PUT to R2 -> Confirm HeadObject & Commit.
Go: AWS SDK v2R2: Zero EgressDB: 2-Phase Quota

2. Ingestion Paradigms: Server Proxy vs Direct Presigned URL Matrix

Before walking through the code implementation, let us contrast the trade-offs between legacy server multipart proxies and modern direct-to-storage presigned uploads:

Architectural DimensionMultipart Server Proxy (Legacy)Direct (Modern)
Binary Data PathClient ➔ API Server ➔ S3 Storage (2 Slow Hops)Client ➔ Cloudflare R2 (1 Direct Hop)
Backend RAM FootprintScales linearly with file size and usersNear Zero (Stays rock-solid < 35 MB RAM)
Scalability BottleneckServer RAM, TCP thread pools, disk buffersVirtually unlimited elasticity backed by R2 edge
Slow Client Network ImpactTies up server worker threads (triggers 502 errors)Zero backend impact (Isolated at Cloudflare edge)
Network Bandwidth CostDual bandwidth charges on API serversZero backend ($0 R2 Policy)
Storage Credential SafetySafe (Credentials stay in backend)Highly Safe (Client receives short-lived tokens only)
SaaS Quota EnforcementDelayed (File finishes upload before quota check)Strict & (Verified prior to URL issuance)

3. Three Critical Problems Behind Traditional Upload Systems

Problem 1: Concurrent Burst Attacks Exploiting Quota Checks (Storage Double-Spend)

If a system simply checks remaining quota without an reservation mechanism:

  • A user on a Free Plan (500 MB limit) with 100 MB remaining opens 5 browser tabs simultaneously.
  • In each tab, the user initiates an upload of a 90 MB file.
  • If the backend checks if (used + 90MB > 500MB), all 5 requests pass because at that moment, remaining quota is still 100 MB!
  • Consequently, the tenant uploads 450 MB of new files, causing a 350 MB leak.

Solution 1: Atomic Two-Phase Quota Reservation & Reconciliation

We enforced a Three-Way Handshake:

  1. 1Phase 1 (Soft Reservation): Lock the tenant subscription record atomically using SELECT current_storage_bytes, reserved_storage_bytes FROM tenant_subscriptions FOR UPDATE. Add file bytes to reserved_storage_bytes. If total exceeds limits, reject immediately with HTTP 403 Forbidden.
  2. 2Phase 2 (Direct Upload): The client streams directly to Cloudflare R2.
  3. 3Phase 3 (Hard Commit): The client triggers the confirmation endpoint. The backend validates real object size via HeadObject, transitions bytes from reserved to current, and completes the ticket. A background cron reconciles expired abandoned reservations.

Problem 2: Binary Size Spoofing (Content-Length Exploitation)

A malicious actor could request a declaring a 1 MB file, but stream a 2 GB payload directly to R2 to bypass quota restrictions.

Solution 2: Cryptographic Header Sealing on Presign Creation

When generating the presigned PUT URL with the AWS SDK for Go v2, we bind ContentLength and ContentType directly into the cryptographic signature. Cloudflare R2 automatically rejects any request whose payload fails to match the exact byte size declared in the signature.


Problem 3: Explosive Egress Bandwidth Bills on Conventional Storage

On conventional object storage providers like AWS S3, outbound data transfer is charged around $0.09 per gigabyte. For SaaS platforms with thousands of active tenants reading and downloading documents daily, fees frequently dwarf the cost of raw storage.

Solution 3: Cloudflare R2 Zero-Egress Architecture

Cloudflare R2 provides an S3-compatible API with a revolutionary guarantee: $0 Data Transfer Out Fees. This policy eliminated over 70% of our ongoing cloud storage operational costs.


4. Production War Stories: The OOM Crash & 1.8 TB Quota Leak Incident

During our third quarter in production, our engineering team experienced one of our most severe infrastructure alerts:

Production Incident & Bottleneck Case

Production Incident Timeline: Outage & Massive Storage Leak

  • Trigger: An enterprise tenant executed an automated bulk migration of 12,000 document archives (average 80 MB per file) using 40 parallel background workers.
  • System Failure: Memory buffers on Go API skyrocketed from 150 MB to 7.8 GB in 45 seconds, invoking the Linux Kernel and plunging our cluster into crash loops for 40 minutes.
  • Secondary Failure: Non- quota checks allowed that tenant to store 1.8 TB of assets on a subscription tier intended for 250 GB.

Following this crisis, we dismantled our multipart proxy pipeline entirely and replaced it with direct Cloudflare R2 presigned PUT streaming backed by two-phase reservations. Since rolling out this architecture, our backend RAM utilization has remained flat below 32 MB, even when hundreds of enterprise users upload gigabytes concurrently.


5. Production Go & Next.js Implementation

Here is our modular production implementation cleanly decoupling Go quota management from Next.js browser streaming:

A. Cloudflare R2 Client Configuration in Go (AWS SDK v2)

Golang Implementation Reference51 baris kode
> // pkg/storage/r2_service.go | package storage ...Klik untuk melihat implementasi lengkap →

B. Atomic Quota Reservation & Presigned URL Handler (Go)

Golang Implementation Reference109 baris kode
> // pkg/storage/presign_handler.go | package storage ...Klik untuk melihat implementasi lengkap →

C. Confirmation Handler & HeadObject Verification (Go)

Golang Implementation Reference53 baris kode
> func (s *R2StorageService) ConfirmUpload(ctx context.Context, db *sql.DB, tenantID, uploadID string) error { | tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) ...Klik untuk melihat implementasi lengkap →

D. Frontend Client Uploader in Next.js (TypeScript)

A React client component handling direct binary PUT streams with XMLHttpRequest for continuous upload telemetry:

TSX Reference146 baris kode
> // src/components/storage/R2DirectUploader.tsx | "use client"; ...Klik untuk melihat implementasi lengkap →

6. Real Production Metrics: Before vs After

We evaluated this architecture via load tests simulating 500 users streaming 50 MB media assets simultaneously:

Performance MetricMultipart Proxy (Before)Direct R2 (After)Production Impact
Go Backend RAM Usage6,400 MB (Near crash threshold)32 MB (Constant flat line)99.5% Memory Efficiency
API Throughput42 (Choked by I/O buffers)4,800+ ( JSON)114x Ingestion Expansion
Average Socket Hold Time18,400 ms (Tied to user upload speed)12 ms (Only cryptographic signing)99.9% Faster Socket Release
Monthly (10 TB Data)$900 / month (AWS S3 standard)$0 / month (Cloudflare R2 zero-)100% Free Bandwidth
Quota Leak14.2% leak on burst spikes0.00% (Guaranteed Two-Phase)Zero Quota Leakage

7. 5 Golden Security Rules for Direct Storage Uploads

  1. 1Strict CORS Configurations: Ensure your R2 bucket only accepts PUT and HEAD requests from your official SaaS application domains (https://app.yourdomain.com). Never leave CORS set to wildcards *.
  2. 2Bind Content-Type and Content-Length to the Signature: Always sign exact byte lengths and MIME types so malicious actors cannot substitute multi-gigabyte binaries under a 1 MB ticket.
  3. 3Keep Expiration Windows Tight: Enforce a maximum URL lifetime of 15 minutes. Treat as single-use credentials.
  4. 4Validate Magic Bytes in Asynchronous Workers: Use background worker routines to inspect the initial 512 bytes (magic numbers) of stored assets to confirm authentic file signatures before assets become publicly accessible.
  5. 5Run Scheduled Quota Workers: Schedule a cron routine every 30 minutes to release unconfirmed reserved_storage_bytes from abandoned uploads, ensuring tenant quotas never remain locked.
S3 Presigned Upload Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →