Taryana
Kembali ke Semua Artikel Teknis/Arsitektur Direct Upload Cloudflare R2 dengan S3 Presigned URL, Go, dan Next.js pada Sistem SaaS Multi-Tenant
GoCloudflare R2S3 Presigned URLNext.jsSaaSArchitecture

Arsitektur Direct Upload Cloudflare R2 dengan S3 Presigned URL, Go, dan Next.js pada Sistem SaaS Multi-Tenant

Belajar dari pengalaman nyata menangani insiden OOM server, kebocoran kuota 1.8 TB, dan tagihan egress: bagaimana arsitektur direct upload Cloudflare R2 dengan Go dan Next.js menyelamatkan efisiensi memori hingga 99,5%.

1. Mengapa Mengunggah File Melalui Server Backend Pasti Melumpuhkan Sistem?

Pernahkah kamu mengalami mimpi buruk di mana server aplikasi backend-mu mendadak mati suri karena kehabisan memori (Linux ) tepat saat beberapa pengguna mengunggah dokumen PDF atau video berukuran puluhan megabyte secara bersamaan? Atau pernahkah kamu terperangah melihat tagihan cloud membengkak ratusan dolar hanya gara-gara biaya transfer data keluar ( bandwidth)?

Pengalaman menegangkan itulah yang pernah saya dan tim hadapi ketika pertama kali merilis platform SaaS manajemen dokumen kolaboratif. Di awal pengembangan, kami menggunakan pendekatan paling umum:

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

Di atas kertas, kode controller backend tampak sederhana dan berjalan normal saat diuji dengan file 2 MB di laptop pengembang. Namun, begitu platform dilempar ke pengguna enterprise dengan file-file berukuran 50 MB hingga 500 MB, arsitektur proxy tersebut langsung runtuh:

  • Server Crash Berulang (Out-Of-Memory Panic): Ketika puluhan pengguna mengunggah file besar di jaringan kantor yang padat, thread pool backend tertahan menampung buffer binary di RAM. Memori server melonjak dari 150 MB menjadi 7,8 GB dalam hitungan detik, memicu dan merestart backend berulang kali ().
  • Koneksi Klien Lambat Menguras Socket (Slow-Client Starvation): Pengguna di jaringan ponsel yang lambat menahan soket HTTP server selama bermenit-menit hanya untuk menyelesaikan upload satu file. Akibatnya, pengguna lain yang ingin melakukan login atau input data sederhana tercekik dengan error 502 Bad Gateway.
  • Bencana Tagihan Ganda: Server backend kami harus membayar dua kali biaya jaringan: saat file masuk ke server, dan saat server menyalurkannya kembali ke cloud storage.
  • Storage Tenant (Double-Spend): Karena kuota baru dipotong setelah file selesai diunggah ke server, pengguna nakal dapat membuka 5 tab secara bersamaan untuk mengunggah file besar, menimbun hingga 1,8 TB data di atas paket langganan yang seharusnya dibatasi 250 GB!

Dari rangkaian masalah tersebut, kami menarik satu prinsip arsitektural mutlak: data binary berukuran besar tidak boleh pernah menyentuh proses memori server backend aplikasi. Server backend bertindak sebagai penjaga gerbang (Identity & Quota Gatekeeper), sedangkan aliran binary dialirkan langsung (direct streaming) dari browser ke object storage melalui S3 .

Executive Summary & Core Insight

Blueprint Solusi: Direct Cloudflare R2 Upload dengan Two-Phase Reservation Untuk membasmi server dan mengeliminasi , kami membangun arsitektur 3 pilar:

  1. 1Go Quota Gatekeeper & Presign Generator: Klien hanya mengirimkan metadata kecil (nama file, ukuran byte, tipe MIME). Backend Go mengecek kuota paket subscription secara di PostgreSQL/Redis, mereservasi kuota sementara (soft reservation), dan menerbitkan URL R2 bertanda tangan kriptografis dengan masa kedaluwarsa ketat 15 menit.
  2. 2Direct Streaming Browser ke Cloudflare R2: Browser Next.js mengunggah binary langsung ke Cloudflare R2 via HTTP PUT dengan pelacakan progres real-time. Cloudflare R2 membebaskan biaya transfer keluar ($0 Fee), memangkas tagihan storage secara radikal.
  3. 3 Confirmation Callback (): Setelah transfer sukses di R2, backend memverifikasi keberadaan dan ukuran file aktual langsung ke R2 (HeadObject), lalu mengonversi kuota reservasi menjadi alokasi permanen (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. Peta Topologi dan Matriks Komparasi Paradigma Upload

Sebelum membedah implementasi kode, mari kita bandingkan perbedaan mendasar antara pola lama (Server Multipart Proxy) dan pola modern (Direct-to-Storage ):

Dimensi RekayasaServer Proxy Upload (Pola Lama)Direct Upload (Pola Modern)
Alur Data BinaryKlien ➔ Server API ➔ S3 Storage (2x Hop Lambat)Klien ➔ Cloudflare R2 (1x Hop Langsung)
Konsumsi RAM BackendMembengkak proporsional terhadap ukuran file & userSangat Ringan (Stabil konstan < 35 MB RAM)
Bottleneck SkalabilitasKapasitas RAM, thread socket, dan buffer serverElastis tanpa batas mengikuti kapasitas Anycast edge
Dampak Klien Jaringan LambatMenahan thread server hingga memicu 502 Bad GatewayNol dampak pada backend (Ditangani langsung oleh R2)
Biaya Jaringan (/)Membayar bandwidth ganda di server aplikasiNol backend ($0 R2 Policy)
Keamanan Kredensial StorageAman (Kredensial disimpan di server)Sangat Aman (Klien hanya terima token sementara)
Penegakan Kuota StorageTerlambat (File sudah terunggah baru kuota dicek)Akurat & (Diverifikasi sebelum izin terbit)

3. Tiga Masalah Nyata di Balik Kerusakan Sistem Upload Tradisional

Masalah 1: Serangan Concurrent Burst yang Membocorkan Kuota (Double-Spend Storage)

Jika sistem hanya memeriksa kuota di awal tanpa mekanisme reservasi :

  • Pengguna paket Free (kuota 500 MB) yang sisa kuotanya tinggal 100 MB membuka 5 tab browser sekaligus.
  • Di masing-masing tab, pengguna mengunggah file berukuran 90 MB.
  • Jika pemeriksaan kuota hanya berupa if (used + 90MB > 500MB), kelima request akan sama-sama lolos karena saat dicek, sisa kuota masih 100 MB!
  • Akibatnya, tenant berhasil menyimpan 450 MB data baru dan kuota bocor sebesar 350 MB.

Solusi 1: Atomic Two-Phase Quota Reservation & Reconciliation

Kami menerapkan model Three-Way Handshake:

  1. 1Fase 1 (Soft Reservation): Mengunci baris kuota di database menggunakan transaksi SELECT current_storage_bytes, reserved_storage_bytes FROM tenant_subscriptions FOR UPDATE. Tambahkan ukuran file ke reserved_storage_bytes. Jika melebihi batas, tolak dengan HTTP 403 Forbidden.
  2. 2Fase 2 (Direct Upload): Klien mengunggah langsung ke R2.
  3. 3Fase 3 (Hard Commit): Klien memanggil endpoint konfirmasi. Backend memeriksa ukuran riil via HeadObject, memindahkan byte dari reserved ke current, dan menandai status tiket selesai. Cron worker membersihkan reservasi yang menggantung jika klien membatalkan upload.

Masalah 2: Pemalsuan Ukuran Binary (Content-Length Spoofing)

Klien nakal dapat meminta dengan melaporkan ukuran file 1 MB, namun saat menembak URL R2, klien mengirim file berukuran 2 GB untuk membobol kuota.

Solusi 2: Kriptografi Presign URL dengan Penguncian Header Mutlak

Saat memanggil fungsi presign di Go AWS SDK v2, kita mengunci header ContentLength dan ContentType ke dalam tanda tangan kriptografis URL. Cloudflare R2 secara otomatis menolak dengan status HTTP 403 jika payload yang dikirimkan klien tidak sesuai persis dengan ukuran byte yang ditandatangani.


Masalah 3: Tagihan Egress Membengkak Gila-Gilaan pada Storage Konvensional

Pada cloud storage konvensional seperti AWS S3, biaya transfer data keluar (data fee) dipatok sekitar $0,09 per gigabyte. Untuk aplikasi SaaS dengan ribuan pengguna yang mengunduh dan membaca dokumen setiap hari, tagihan bandwidth keluar sering kali jauh lebih mahal daripada biaya sewa penyimpanan itu sendiri.

Solusi 3: Pemanfaatan Cloudflare R2 Bebas Biaya Egress

Cloudflare R2 menyediakan API yang 100% kompatibel dengan protokol AWS S3, namun dengan kebijakan revolusioner: $0 Data Fee. Hal ini memungkinkan platform kami menghemat ratusan hingga ribuan dolar per bulan untuk transfer data dokumen.


4. Kasus Nyata di Medan Produksi: Insiden OOM Server dan Kebocoran Kuota 1.8 TB

Pada kuartal ketiga implementasi SaaS manajemen dokumen kami, tim mengalami salah satu insiden paling genting:

Production Incident & Bottleneck Case

Kronologi Insiden Produksi: Server & Dokumen

  • Pemicu: Salah satu tenant enterprise melakukan migrasi massal 12.000 arsip dokumen (rata-rata 80 MB per file) menggunakan skrip otomatis dengan 40 worker paralel.
  • Dampak: Buffer memori di Go melonjak dari 150 MB menjadi 7,8 GB dalam 45 detik, memicu Linux Kernel dan merestart berulang kali selama 40 menit.
  • Dampak Tambahan: Celah verifikasi kuota non- membuat tenant tersebut berhasil menimbun 1,8 TB file pada paket langganan yang seharusnya dibatasi 250 GB.

Setelah insiden tersebut, tim kami membongkar total jalur upload proxy multipart dan menggantinya dengan model Direct Cloudflare R2 Presigned PUT dengan reservasi . Sejak arsitektur baru diterapkan, beban RAM backend tetap datar di bawah 32 MB meskipun ratusan klien mengunggah file gigabyte secara serentak.


5. Implementasi Kode Go & Next.js Skala Produksi

Berikut adalah arsitektur kode produksi yang memisahkan tanggung jawab secara bersih antara gatekeeper Go dan uploader Next.js:

A. Konfigurasi Client Cloudflare R2 di Go (AWS SDK v2)

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

B. Handler Reservasi Kuota & Pembuat Presigned URL (Go)

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

C. Handler Konfirmasi & Validasi HeadObject (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 di Next.js (TypeScript)

Komponen React di browser yang menangani streaming langsung ke R2 menggunakan XMLHttpRequest untuk pelacakan persentase progres yang akurat:

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

6. Hasil Riil di Produksi: Metrik Sebelum vs Sesudah

Kami menguji arsitektur ini dengan melakukan load test 500 pengguna aktif yang mengunggah file media 50 MB secara simultan:

Metrik Kinerja SistemMultipart Proxy (Sebelumnya)Direct R2 (Sekarang)Dampak Rekayasa Nyata
Konsumsi RAM Server Go6.400 MB (Mendekati limit crash)32 MB (Stabil konstan)Efisiensi Memori 99,5%
Throughput Transaksi Backend42 (Tersumbat file I/O)4.800+ ( JSON)Kapasitas Naik 114x Lipat
Durasi Penahanan Soket API18.400 ms (Mengikuti kecepatan klien)12 ms (Hanya signing kriptografis)99,9% Lebih Cepat Lepas Soket
Bulanan (10 TB Data)$900 / bulan (Standar AWS S3 )$0 / bulan (Cloudflare R2 zero-)100% Bebas
Tingkat 14,2% bocor saat burst0,00% (Terkunci via Two-Phase)Zero

7. 5 Kaidah Emas Keamanan & Rekonsiliasi Upload di Produksi

  1. 1Konfigurasi CORS Terbatas: Pastikan bucket R2 hanya menerima method PUT dan HEAD dari origin domain aplikasi SaaS resmi kamu (https://app.yourdomain.com). Jangan pernah menggunakan wildcard *.
  2. 2Kunci Header Content-Type dan Content-Length pada Signature: Saat memanggil PresignPutObject, sertakan ukuran byte dan tipe konten eksplisit agar pengguna tidak bisa menyelundupkan file raksasa dengan izin file kecil.
  3. 3Masa Berlaku URL Singkat: Tetapkan batas kedaluwarsa maksimal 15 menit. URL ini harus diperlakukan sebagai token transaksi sekali pakai.
  4. 4Validasi File Magic Bytes di Background Worker: Gunakan worker asinkron untuk membaca 512 byte pertama (magic numbers) file di storage guna memverifikasi integritas tipe data riil sebelum aset dipublikasikan ke publik.
  5. 5Jalankan Cron Job: Pasang cron job berkala setiap 30 menit untuk mengembalikan nilai reserved_storage_bytes yang tiketnya kedaluwarsa tanpa pernah dikonfirmasi, memastikan kuota tenant tidak menggantung akibat masalah jaringan pengguna.
S3 Presigned Upload Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →