Edge artinya kodemu berjalan di banyak data center di seluruh dunia, dekat dengan pengguna, bukan di satu server. Durable Object (dari Cloudflare) adalah satu objek dengan nama unik yang hidup di satu tempat, punya storage sendiri, dan menjalankan satu permintaan pada satu waktu. Setiap request dari mana pun yang menyebut nama itu sampai ke objek yang sama. Bayangin papan order Laundry Bu Rini dipegang satu juru tulis. Siapa pun yang mau tahu atau mengubah status, dari Jakarta atau Surabaya, bicara ke juru tulis itu. Karena cuma satu orang yang memegang papan, gak ada dua tulisan yang saling menimpa.
🧪 Tebakan cepat: Tablet Mas Eko di Jakarta dan HP pelanggan di Surabaya sama-sama tersambung lewat
env.PAPAN.getByName('laundry-bu-rini'). Berapa object yang ada, dan perlukah Redis supaya kabar dari Mas Eko sampai ke HP itu? Jawabannya ada di bagian "Satu nama, satu object".
Satu nama, satu object
Kodenya dua bagian: Worker (fungsi yang menerima request di data center terdekat pengguna) dan class Durable Object.
import { DurableObject } from 'cloudflare:workers'
export class Papan extends DurableObject {
async fetch(request) {
const user = new URL(request.url).searchParams.get('user') ?? 'tamu'
const [client, server] = Object.values(new WebSocketPair())
this.ctx.acceptWebSocket(server) // Hibernation API: runtime yang memegang koneksinya
server.serializeAttachment({ user }) // ikut tersimpan walau object tidur
const orders = this.ctx.storage.sql.exec('SELECT * FROM orders ORDER BY id').toArray()
server.send(JSON.stringify({ type: 'snapshot', orders }))
return new Response(null, { status: 101, webSocket: client })
}
async webSocketMessage(ws, message) {
const { user } = ws.deserializeAttachment()
const m = JSON.parse(message)
this.ctx.storage.sql.exec('UPDATE orders SET status = ? WHERE id = ?', m.status, m.id)
const kabar = JSON.stringify({ type: 'status', id: m.id, status: m.status, by: user })
for (const s of this.ctx.getWebSockets()) s.send(kabar) // semua socket papan ini ada di sini
}
async webSocketClose(ws, code, reason) {
ws.close(code, reason)
}
}
export default {
async fetch(request, env) {
if (new URL(request.url).pathname === '/papan' && request.headers.get('Upgrade') === 'websocket') {
return env.PAPAN.getByName('laundry-bu-rini').fetch(request) // nama yang sama, object yang sama
}
return new Response('Not found', { status: 404 })
},
}
(Konstruktornya membuat tabel orders dan mengisi #12 sampai #15; ada di kode Lab.) Dijalankan lokal dengan wrangler dev (wrangler 4.149.0, tanpa deploy dan tanpa login), dua client ws, Mas Eko dan Bu Rini:
rini <- {"type":"snapshot","orders":[{"id":12,"nama":"Pak Darto","kg":3,"status":"Dicuci"}, ...]}
eko -> {"type":"status","id":12,"status":"Siap diambil"} (49 byte)
eko <- {"type":"status","id":12,"status":"Siap diambil","by":"eko"}
rini <- {"type":"status","id":12,"status":"Siap diambil","by":"eko"}
Setelah wrangler dev di-restart, snapshot berikutnya sudah berisi #12 Siap diambil: storage SQLite milik object itu tersimpan. GET /papan tanpa header Upgrade dijawab 404.
Itu jawaban tebakannya: satu object, dan gak perlu Redis. Semua WebSocket papan itu tersambung ke object yang sama, jadi menyiarkan cukup satu loop atas this.ctx.getWebSockets(). Bandingkan dengan pelajaran Pub/Sub: dua proses Node butuh sticky session dan Redis adapter.
Hibernation: object tidur, koneksi tetap
this.ctx.acceptWebSocket(server) memakai WebSocket Hibernation API: runtime Cloudflare yang memegang koneksinya, dan memanggil webSocketMessage, webSocketClose, atau webSocketError saat ada kejadian. Menurut dokumentasinya:
- Setelah 10 detik tanpa request atau event (gak ada timer, gak ada pekerjaan yang belum selesai), object berhibernasi. Koneksi WebSocket-nya tetap tersambung. Setelah 70 sampai 140 detik sepi, object dikeluarkan dari memori.
- Saat pesan berikutnya datang, object dibuat lagi: konstruktornya jalan lagi, dan variabel di memori (
this.jumlah = 0) kembali ke awal. Simpan yang penting dithis.ctx.storage, atau per koneksi diserializeAttachment(paling besar 16.384 byte). - Selama hibernasi gak ada biaya durasi.
- Cara lama,
server.accept()laluaddEventListener('message', ...), adalah API WebSocket standar. Object yang memakainya gak bisa hibernasi, jadi tetap di memori dan tetap ditagih selama koneksinya terbuka.
Satu thread per object
Setiap object single-threaded: satu event diproses sampai selesai sebelum event berikutnya. Dua tablet yang mengubah #12 bersamaan gak bisa saling menimpa di tengah jalan, tanpa lock. Imbalannya, satu object punya batas: dokumentasi menyebut batas lunak 1.000 request per detik per object, dan setiap pesan WebSocket yang masuk juga diproses oleh thread yang sama.
Jadi pilih ukuran object sesuai bebannya:
- Satu object per papan (
laundry-bu-rini): cocok untuk laundry. Puluhan perubahan sehari, belasan layar. - Satu object per order (
order:12): HP pelanggan cuma tersambung ke order miliknya, dan beban terbagi ke banyak object. Layar staf yang butuh semua order perlu cara lain, misalnya object papan yang dikabari oleh object order.
Object dibuat dekat dengan tempat pertama kali ia diminta, dan gak disalin ke region lain. Untuk laundry di Jakarta, artinya papannya hidup dekat Jakarta, dekat dengan semua penggunanya: itu keuntungan latensinya dibanding server realtime di Amerika yang setiap kabarnya menyeberang Pasifik. Pengguna di Surabaya masuk lewat Worker di data center terdekatnya, lalu diteruskan lewat jaringan Cloudflare ke object yang sama.
Batas dan harga
Dari dokumentasi Cloudflare, dicek 11 Oktober 2026 (halaman harga diperbarui 30 September 2026):
| Workers Free | Workers Paid | |
|---|---|---|
| Request | 100.000 per hari | 1 juta per bulan, lalu $0,15 per juta |
| Durasi | 13.000 GB-detik per hari | 400.000 GB-detik per bulan, lalu $12,50 per juta GB-detik |
| Storage | SQLite saja | SQLite, 10 GB per object |
- Pesan WebSocket masuk dihitung 20 banding 1: 100 pesan masuk = 5 request. Pesan keluar gratis.
- Pesan WebSocket yang diterima paling besar 32 MiB.
- Laundry Bu Rini dengan beberapa ratus pesan sehari muat di paket gratis.
Pilihan lain
- PartyKit: sejak April 2024 bagian dari Cloudflare; library-nya sekarang
partyserver(0.5.10) di atas Durable Objects, dengan room, presence, dan integrasi Yjs. - Layanan terkelola: Ably dan Pusher (dan Supabase Realtime dari pelajaran sebelumnya) menjalankan server realtime untukmu. Kamu membayar per pesan atau koneksi, dan gak menulis kode server.
- Server sendiri: Socket.IO dengan Redis adapter, atau Laravel Reverb. Kamu yang mengurus proses, sticky session, dan deploy.
⚠️ Jebakan yang sering ketemu
- State di variabel memori. Hilang setelah hibernasi, karena konstruktornya jalan lagi. Pakai storage atau attachment.
server.accept()di Durable Object. Object gak bisa tidur, dan durasi ditagih selama koneksinya hidup. Pakaithis.ctx.acceptWebSocket().- Satu object untuk segalanya di skala besar. Satu thread, batas lunak 1.000 request per detik. Bagi per order atau per cabang.
- Mengira state-nya ada di setiap region. Object hidup di satu tempat; yang dekat dengan pengguna adalah Worker-nya.
TL;DR: Durable Object adalah satu objek bernama unik dengan storage sendiri, hidup di satu tempat, single-threaded. Worker di edge meneruskan request ke env.PAPAN.getByName('laundry-bu-rini'), jadi semua WebSocket papan itu berkumpul di satu object dan siaran cukup satu loop atas this.ctx.getWebSockets(), tanpa Redis dan tanpa sticky. Dengan Hibernation API (acceptWebSocket, webSocketMessage, webSocketClose), object tidur setelah 10 detik sepi tanpa memutus koneksi dan tanpa biaya durasi, tapi konstruktornya jalan lagi, jadi simpan state di storage atau serializeAttachment. Batasnya sekitar 1.000 request per detik per object; pesan masuk dihitung 20 banding 1, pesan keluar gratis. Alternatifnya PartyKit (partyserver), layanan terkelola seperti Ably dan Pusher, atau server sendiri.