Edge Real-time (Durable Objects) — Realtime

Edge Real-time Traditional: satu WebSocket server di satu region. Edge: WebSocket server di setiap edge location. Cloudflare Durable Objects memungkinkan…

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:

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:

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

Pilihan lain

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari