Hampir dua bulan, jalur distribusi media saya seperti ini:
Browser
↓
server kerja
↓
Nginx
↓
proxy_cache lokal
↓ cache MISS
Bunny Storage
Gambar, video, dan audio berada di Bunny Storage. Pengguna tidak mengakses Bunny secara langsung. Nginx meminta object dari [https://storage.bunnycdn.com](https://storage.bunnycdn.com), menambahkan AccessKey di sisi server, menyimpan response ke SSD lokal, lalu melayani URL publik sendiri.
Ruang lingkupnya penting: ini bukan pengujian Bunny CDN. Saya tidak menggunakan Bunny CDN dalam eksperimen ini. Storage Zone + Pull Zone/CDN adalah arsitektur distribusi yang berbeda, dan saya tetap menganggap Bunny CDN produk yang sangat bagus. Yang saya uji adalah Bunny Storage sebagai runtime origin untuk cache Nginx saya sendiri.
Mengapa arsitektur awal tampak masuk akal
Workload berisi banyak object media immutable: AVIF, JPEG, PNG, MP4, WebM, audio, dan file statis lain, mulai dari gambar kecil sampai video yang jauh lebih besar. Saya tidak ingin seluruh dataset berada di application server, tetapi saya ingin Nginx sendiri mengontrol URL publik, cache policy, redirect, missing-file behavior, Range, dan fallback.
┌── HIT ── SSD lokal
│
Browser → cache Nginx
│
└── MISS ── Bunny Storage
Pada HIT, desainnya bekerja sangat baik. Nginx melayani object langsung dari disk lokal sehingga Bunny sama sekali tidak terlibat. Keberhasilan ini justru lama menutupi bagian terpenting: cold MISS.
File populer menyembunyikan latency origin sebenarnya
Gambar yang sering diminta cenderung tetap berada di cache. File lama yang jarang dibuka lebih mudah ter-evict. Karena itu, halaman lama tanpa sengaja menjadi benchmark origin.
Satu snapshot Juli menunjukkan sekitar 36 GB media cache, lebih dari 355.000 cache file, batas sekitar 35 GB, dan root filesystem terisi sekitar 93%. Snapshot lain mendekati 400.000 entry. Cache sebesar itu tetap terbatas.
Pelajarannya: HIT yang cepat tidak membuktikan origin cepat. Itu hanya membuktikan cache lokal cepat.
Log production menunjukkan kegagalan sebelum body file mulai ditransfer
upstream timed out
while connecting to upstream
dan:
upstream timed out ... while SSL handshaking to upstream
Ini berbeda dari file besar yang sekadar membutuhkan waktu lama untuk diunduh. Dalam kasus ini Nginx masih mencoba membuat koneksi upstream atau menyelesaikan TLS.
Pada snapshot diagnostik 30 Juli, 5.000 baris terakhir media error log berisi 317 kecocokan upstream timeout. Snapshot sehari sebelumnya berisi 578. Itu jumlah baris log, bukan jumlah user unik, tetapi jelas bukan kejadian tunggal.
Kasus buruk tidak terbatas pada satu IP storage
109.61.89.53
109.61.89.54
109.61.89.55
109.61.89.57
79.127.226.193
Saya melihat failure pada beberapa address dari set tersebut. Dari sudut pandang server saya, masalahnya ada pada perilaku jalur origin eksternal secara keseluruhan, bukan satu IP yang selalu buruk.
Log saya tidak dapat membuktikan apakah akar penyebabnya storage backend, routing, peering, jalur provider saya menuju Bunny, balancing, atau faktor jaringan lain. Yang bisa dibuktikan adalah beberapa cold MISS tidak berhasil membangun koneksi Bunny Storage dalam waktu yang diharapkan.
Satu pengukuran memperlihatkan long tail dengan jelas
109.61.89.53 total ≈ 29.8 ms
109.61.89.54 total ≈ 28.5 ms
109.61.89.57 total ≈ 42.7 ms
79.127.226.193 total ≈ 43.6 ms
Satu jalur sangat berbeda:
109.61.89.55
TCP 1.017782 s
TLS 1.048306 s
TOTAL 1.054474 s
Ini bukan berarti Bunny Storage selalu lambat satu detik. Kebanyakan pengukuran hanya puluhan ms. Yang penting adalah variance: hostname origin yang sama bisa menghasilkan jalur koneksi lebih dari satu detik sebelum transfer object berguna dimulai.
Pada halaman media, tail latency lebih terasa daripada rata-rata
Jika 79 gambar selesai dalam 30–50 ms dan satu gambar membutuhkan satu detik, nilai rata-rata masih terlihat bagus. User tidak melihat rata-rata; user melihat satu kotak kosong. Karena itu p95, p99, dan cold MISS paling lambat menjadi lebih penting bagi saya.
Video memperlihatkan masalah kedua yang benar-benar berbeda
Browser dapat meminta byte Range:
Range: bytes=0-1048575
dan setelah seek:
Range: bytes=50000000-51048575
Dengan 206 Partial Content, video dapat mulai atau berpindah posisi tanpa mengunduh semua byte sebelumnya.
Cache Range pertama saya memecah satu video fisik menjadi banyak cache key
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_cache_key "$scheme|$host|$request_uri|range=$http_range";
proxy_cache_valid 200 206 301 302 30d;
Range browser bersifat arbitrary. bytes=0-1048575, bytes=0-999999, atau offset setelah seek menghasilkan key berbeda untuk MP4 yang sama. Saya tidak mengukur berapa GB cache yang dikonsumsi fragmentasi ini, jadi saya tidak akan mengarang angka.
Saya beralih ke slice tetap 1 MB
Nginx Slice module dapat menormalkan object besar menjadi chunk tetap:
0–1 MB
1–2 MB
2–3 MB
...
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$scheme|$host|$uri|$slice_range";
proxy_cache_valid 200 206 30d;
Untuk reuse cache ini jauh lebih menarik. Kemudian production mulai mengeluarkan:
etag mismatch in slice response while reading response header from upstream
Slice MP4 gagal pada konsistensi ETag
Pada 30 Juli, subrequest slice berulang untuk satu MP4 menghasilkan etag mismatch in slice response sementara request menuju address seperti 109.61.89.53, 109.61.89.57, 79.127.226.193, dan 109.61.89.55. Pada 31 Juli kelas error yang sama muncul lagi pada MP4 lain.
Saya sempat menyebutnya secara informal sebagai “signature berbeda” pada potongan. Secara teknis yang relevan adalah ETag, bukan AccessKey, signed URL, atau tanda tangan kriptografis.
Mengapa Nginx tidak menggabungkan slice dengan ETag berbeda
slice 1 → bytes 0–1 MB → ETag A
slice 2 → bytes 1–2 MB → ETag A
konsisten. Tetapi:
slice 1 → ETag A
slice 2 → ETag B
membuat Nginx tidak dapat memastikan keduanya berasal dari representation yang sama. Menggabungkannya begitu saja bisa mencampur byte dari versi file yang berbeda. Artikel Nginx tentang byte-range caching menjelaskan pemeriksaan validator ini.
Log saya membuktikan Nginx melihat ETag tidak kompatibel antar-slice MP4 yang sama. Log tersebut tidak membuktikan penyebab internal persisnya karena saya tidak merekam literal ETag setiap subrequest. Perpindahan upstream address membuat ketidakkonsistenan response backend masuk akal, tetapi itu tetap inferensi.
Ini tidak berarti Bunny Storage tidak mendukung Range
Range bekerja dan saya memang meng-cache 206 Partial Content. Masalah spesifiknya adalah kombinasi Nginx Slice, beberapa response storage, dan konsistensi ETag yang dibutuhkan untuk assembly yang aman.
Video juga dapat terkena origin timeout biasa. Jadi ada dua failure mode terpisah: koneksi/latency origin dan konsistensi Slice/ETag.
Akhirnya saya berhenti menggunakan Slice untuk MP4
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_cache_key "$scheme|$host|$uri";
proxy_no_cache $http_range;
proxy_cache_valid 200 30d;
Partial response tidak disimpan sebagai object lengkap. Ini menghilangkan masalah assembly MP4, tetapi membuat cold Range kembali lebih bergantung pada external origin.
Saya sadar sedang membangun sebagian CDN sendiri di depan Storage
Nginx saya sudah menangani cache lock, stale, background update, Range, 206, Slice, normalized chunk, keepalive, TLS reuse, retry, timeout tuning, dan custom key. Semua masuk akal secara individual, tetapi kebutuhan origin sebenarnya sederhana: menyimpan file immutable dan mengirim byte.
Origin pengganti sengaja dibuat sederhana
Saya memindahkan dataset media ke server dedicated yang pada dasarnya hanya menjalankan Nginx + SSD. Untuk pure static origin, mesin kecil sekitar 1 vCPU, kira-kira 1 GB RAM, dan ratusan GB SSD masuk akal sebagai titik awal jika jaringan dan disk mencukupi.
Ini bukan aturan sizing universal. Bandwidth, ukuran object, concurrency, IOPS, dan MISS rate tetap menentukan. Origin yang saya ukur sekarang memiliki resource lebih besar, jadi peak saat ini bukan benchmark persis untuk mesin 1-vCPU/1-GB.
Arsitektur baru
Browser
↓
server kerja
↓
Nginx + cache media lokal
│
├── HIT
└── MISS
↓
WireGuard
↓
media origin
↓
Nginx
↓
SSD
Origin bersifat private. Server kerja berbicara ke satu private IP tetap. HTTPS publik tetap berakhir di server kerja; traffic ke origin dapat memakai HTTP di dalam tunnel WireGuard yang sudah terenkripsi.
Apa yang hilang dari cold path
server kerja
↓
WireGuard
↓
private IP tetap
↓
Nginx
↓
SSD
Untuk hubungan langsung antardua server ini saya tidak lagi membutuhkan DNS publik, pemilihan salah satu dari beberapa storage address, public Internet path, dan handshake TLS tambahan ke Storage API.
Cold MISS menjadi cukup murah hingga secara visual hampir seperti HIT
Test Range kecil pada file nyata biasanya:
CONNECT ≈ 9.5–12.5 ms
TTFB ≈ 19–23 ms
TOTAL ≈ 19–23 ms
Test 10/10, 20/20 sequential, dan 20/20 parallel berhasil. Pada origin sendiri, request kecil yang sama biasanya sekitar 0.5–0.9 ms.
Ini test kecil Range/TTFB, bukan waktu mengunduh video penuh. Transfer penuh tetap bergantung pada ukuran dan throughput.
Kasus buruk lama dan cold path baru berbeda jauh
Measurement lama yang normal sering berada di 29–44 ms. Kasus buruk yang teramati sekitar 1.054 s. Tiny cold request baru sekitar 20 ms.
Outlier 1.054 s itu dibandingkan 20 ms sekitar 52× secara matematis. Ini bukan klaim “server saya 52× lebih cepat dari Bunny Storage”, melainkan perbandingan dua jalur nyata yang saya ukur.
MP4 juga lebih sederhana sekarang
Pada cold fill, Nginx dapat mengambil MP4 lengkap dari private origin, menyimpan satu cache entry, lalu melayani byte Range browser secara lokal. Upstream Range bisa dihapus saat fill dan proxy_force_ranges memungkinkan response Range lokal.
cold MISS
↓
MP4 lengkap dari private origin
↓
satu file cached
↓
Range client dilayani lokal sebagai 206
Full-object caching juga punya biaya
Jika MP4 cold berukuran 500 MB dan user pertama hanya ingin sepuluh detik, cache fill bisa tetap mentransfer semua 500 MB antara server. Untuk ukuran dan pola saya, trade-off ini dapat diterima. Untuk library video multi-GB yang mayoritas cold, saya akan mempertimbangkan slice cache stabil, HLS/DASH, video CDN, atau Bunny Stream.
Fixed slice tetap berguna untuk media besar lain
Nginx Slice bukan fitur buruk. Dengan object immutable dan origin stabil, ia tetap bermanfaat. Perbedaannya sekarang setiap chunk berasal dari satu origin dan satu representation filesystem yang saya kontrol.
Cache lebih kecil bisa terasa lebih cepat jika MISS murah
Cache lama mencapai sekitar 35–36 GB. Cache baru stabil di sekitar 16 GB karena saya sengaja menjaga sekitar 8 GB tetap kosong dengan min_free.
lama:
HIT = cepat
MISS = kadang menyakitkan
baru:
HIT = cepat
MISS = juga cukup cepat
Cache besar mengurangi jumlah MISS. Origin yang baik mengurangi biaya setiap MISS. Untuk workload ini, hal kedua ternyata lebih penting.
Traffic production nyata mengonfirmasi peran origin
Dalam snapshot 60 detik, server kerja mengirim sekitar 96.84 Mbit/s ke user dan menerima sekitar 7.07 Mbit/s dari media origin. Tidak ada 502, 503, 504, atau upstream timeout baru selama window tersebut.
Dalam snapshot 30 detik berikutnya, public TX adalah 61.36 Mbit/s dan WireGuard RX dari origin 1.40 Mbit/s. Rasio byte origin/public TX adalah 2.28%.
Saya tidak menyebut kebalikannya sebagai cache-hit rate presisi karena public traffic tidak hanya media dan rasio byte bukan rasio request. Namun angkanya menunjukkan origin setelah warm-up terutama menangani cold fill.
Apa yang sekarang akan saya monitor
$upstream_addr
$upstream_connect_time
$upstream_header_time
$upstream_response_time
$upstream_cache_status
$request_time
$status
HIT dan MISS akan saya pisahkan, terutama p95/p99 MISS.
Untuk video saya akan menguji first Range, middle Range, suffix Range, beberapa non-sequential Range untuk seek, cold versus warm, serta memeriksa 206, Content-Range, Content-Length, Accept-Ranges, dan ETag. Jika memakai Slice, saya akan memastikan validator konsisten antar beberapa slice.
Satu cold URL dan 20 cold URL berbeda bukan test yang sama
proxy_cache_lock membantu saat banyak request menginginkan cache key cold yang sama. Ia tidak mengubah 20 file berbeda menjadi satu origin request. Halaman dengan 50 gambar cold berbeda bisa menjadi test origin yang lebih realistis daripada benchmark satu URL berulang.
Self-hosting memberi kontrol, bukan reliability gratis
Private media server membuat critical path lebih mudah dipahami, tetapi backup, kesehatan disk, free space, update, firewall, monitoring, restore, dan redundancy menjadi tanggung jawab saya. Satu origin juga dapat menjadi single point of failure untuk object yang belum cached.
Kapan saya masih akan memilih Bunny
Untuk global delivery, managed redundancy, storage yang tumbuh cepat, atau jika ingin meminimalkan administrasi, saya tetap akan mempertimbangkan Bunny. Saya juga akan menguji secara khusus arsitektur Storage + Pull Zone/CDN.
Eksperimen ini tidak membuktikan satu VPS lebih baik dari Bunny CDN, Bunny Storage selalu lambat, atau Bunny tidak mendukung Range. Ia menunjukkan bahwa untuk workload saya, Bunny Storage sebagai runtime origin langsung bagi cache Nginx menghasilkan cold-path dan sliced-video behavior yang tidak lagi ingin saya kelola.
Aturan yang saya gunakan sekarang
Selama dua bulan saya mengoptimalkan cache size, cache key, Range, 206, Slice, chunk 1 MB, keepalive, TLS reuse, retry, stale, dan timeout. Perubahan paling kuat jauh lebih sederhana: mengganti origin.
Apa yang terjadi jika file yang diminta user tidak ada di cache?
Jika jawabannya adalah origin stabil, TTFB yang dapat diprediksi, Range benar, validator konsisten, dan failure mode yang mudah dipahami, cache hanyalah optimisasi. Jika UX yang baik bergantung pada user tidak pernah mengalami MISS, cache sedang menyembunyikan masalah arsitektur yang lebih dalam.
Untuk workload saya, media origin kecil, private, dan membosankan ternyata lebih cocok. Ia bukan CDN, bukan sihir, dan tidak menghapus tanggung jawab operasi. Ia hanya membuat cold path membosankan. Setelah dua bulan meng-debug gambar, connection timeout, video Range, dan etag mismatch in slice response, itulah yang saya inginkan.