Cloudflare Worker dapat mengakhiri koneksi WebSocket secara langsung. Persoalan arsitektur yang lebih sulit muncul setelah upgrade berhasil: di mana state koneksi disimpan, proses mana yang mengoordinasikan banyak client, dan state apa yang tetap valid ketika runtime sudah tidak berada di memory?

Itu adalah batas yang berbeda:

HTTP request
    |
    | Upgrade: websocket
    v
Worker
    |
    +-- one independent connection
    |
    +-- shared room / session / presence
            |
            v
       Durable Object
            |
            v
       optional hibernation

Menyebut semuanya sebagai “dukungan WebSocket” menutupi perilaku yang justru paling penting ketika sistem masuk production.

Upgrade hanya membentuk batas koneksi

Worker yang bertindak sebagai WebSocket server memeriksa HTTP upgrade request, membuat WebSocketPair, menerima sisi server, lalu mengembalikan sisi client melalui response 101 Switching Protocols.

export default {
  async fetch(request) {
    if (request.headers.get("Upgrade") !== "websocket") {
      return new Response("Expected WebSocket", { status: 426 });
    }

    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();

    server.addEventListener("message", (event) => {
      server.send("echo: " + event.data);
    });

    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  },
};

Ini sudah cukup untuk echo server atau koneksi lain yang seluruh state bergunanya memang hanya milik koneksi tersebut.

WebSocketPair tidak otomatis membuat titik koordinasi bersama. Jika dua client harus melihat membership room, sequence number, presence set, atau game state yang sama, masing-masing koneksi tidak dapat membuat state-nya sendiri lalu tetap menjamin pandangan yang konsisten.

State WebSocket bersama membutuhkan satu titik koordinasi

Chat room memperlihatkan perbedaannya dengan jelas. Anggap ada tiga client yang terhubung:

client A ----\
client B -----+---- room state
client C ----/

State room dapat mencakup:

connected clients
message ordering
presence
rate-limit counters
room metadata

Cloudflare Durable Objects menyediakan satu instance yang dapat dialamatkan untuk batas koordinasi tersebut. Worker dapat melakukan autentikasi atau routing, membentuk Durable Object ID dari room key, lalu meneruskan WebSocket upgrade ke object itu.

Secara konseptual:

Browser
   |
   | wss://
   v
Worker
   |
   | room = "support-42"
   v
Durable Object: support-42
   |
   +-- WebSocket A
   +-- WebSocket B
   +-- WebSocket C

Properti pentingnya bukan sekadar persistence. Object tersebut memberikan satu lokasi eksekusi bagi room untuk koordinasi. Operasi seperti broadcast ke member aktif atau mempertahankan tampilan state berurutan di memory menjadi jauh lebih terdefinisi.

WebSocket standar pada Durable Object dapat membuat object tetap resident

Durable Objects mendukung Web Standard WebSocket API, tetapi koneksi yang diterima dengan API standar membuat object berada dalam kondisi non-hibernatable selama koneksi tersebut mengharuskan object tetap aktif.

Pada workload dengan banyak client yang sebagian besar idle, lifecycle ini dapat mendominasi biaya meskipun volume message rendah.

Hibernation WebSocket API mengubah lifecycle tersebut. Alih-alih memanggil server.accept() di dalam Durable Object, object mendaftarkan socket melalui acceptWebSocket():

import { DurableObject } from "cloudflare:workers";

export class Room extends DurableObject {
  async fetch(request) {
    if (request.headers.get("Upgrade") !== "websocket") {
      return new Response("Expected WebSocket", { status: 426 });
    }

    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    const sessionId = crypto.randomUUID();

    server.serializeAttachment({ sessionId });
    this.ctx.acceptWebSocket(server);

    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  }

  async webSocketMessage(ws, message) {
    const session = ws.deserializeAttachment();

    ws.send(JSON.stringify({
      sessionId: session.sessionId,
      message,
    }));
  }
}

Model event-nya berbeda. Message dikirim ke method seperti webSocketMessage, bukan ke event listener yang dipasang dengan addEventListener.

Perbedaan tersebut merupakan bagian dari kontrak hibernation, bukan sekadar pilihan sintaks lain.

Hibernasi mempertahankan socket, bukan JavaScript memory

Ketika Durable Object memenuhi syarat untuk hibernation, Cloudflare dapat mengeluarkan instance JavaScript-nya dari memory sambil menjaga client WebSocket yang sudah diterima tetap terhubung ke network.

Event WebSocket berikutnya akan membuat ulang object dan menjalankan constructor sebelum event diberikan ke handler.

Lifecycle-nya lebih dekat ke model berikut:

client connected
      |
      v
Durable Object active
      |
      | idle and hibernatable
      v
JavaScript instance removed
      |
      | socket remains connected
      v
message arrives
      |
      v
constructor runs again
      |
      v
webSocketMessage(...)

Apa pun yang hanya disimpan dalam field JavaScript biasa dapat hilang saat transisi tersebut.

Karena itu, map di memory berguna sebagai cache, tetapi tidak aman jika menjadi satu-satunya sumber metadata koneksi yang harus bertahan melewati hibernation.

Attachment menghubungkan identitas socket ke instance baru

serializeAttachment() mengaitkan data yang kompatibel dengan structured clone ke WebSocket yang sudah diterima. Setelah hibernation, deserializeAttachment() dapat mengambil kembali data tersebut dari socket.

Attachment kecil dapat menyimpan identifier seperti:

{
  "sessionId": "2de9...",
  "userId": "user-17",
  "role": "member"
}

Ini berbeda dari durable application storage.

Attachment mengikuti lifecycle koneksi WebSocket. Tempat ini cocok untuk metadata koneksi berukuran kecil yang diperlukan saat instance Durable Object baru membangun kembali working set-nya. Data yang harus tetap ada setelah WebSocket berakhir seharusnya disimpan di Durable Object storage atau data store persisten lain.

Batasnya dapat diringkas seperti ini:

JavaScript field
    -> fast working state
    -> discarded on hibernation

WebSocket attachment
    -> connection-scoped metadata
    -> recoverable while the socket survives

Durable storage
    -> application state
    -> survives connection loss and object eviction

Timer dan outbound connection dapat menggagalkan hibernation

Memakai Hibernation API tidak berarti object pasti akan hibernate.

Durable Object harus idle dan tidak memiliki aktivitas yang mengharuskan instance JavaScript tetap tersedia. Scheduled callback seperti setTimeout atau setInterval, pekerjaan yang masih berlangsung, standard WebSocket API, dan outbound socket aktif dapat membuat object tetap non-hibernatable.

Hal ini penting pada desain heartbeat.

Loop di sisi server seperti:

setInterval(() => {
  sendPingToEveryClient();
}, 30_000);

menciptakan alasan agar instance JavaScript tetap resident. Jika tujuannya hanya pertukaran ping/pong statis, desain yang sadar hibernation sebaiknya tidak membangunkan object hanya untuk menjawab traffic heartbeat tetap.

Cloudflare menyediakan WebSocket auto-response pada Durable Object state untuk kelas message semacam ini, sehingga request yang cocok dapat memperoleh response tetap tanpa membangunkan object yang sedang hibernate.

Outbound WebSocket memiliki lifecycle berbeda

Hibernation berlaku ketika Durable Object menjadi WebSocket server bagi incoming client. Mekanisme ini tidak membuat outbound WebSocket dapat hibernate.

Pertimbangkan bridge berikut:

browser
   |
   v
Durable Object
   |
   | outbound WebSocket
   v
external server

Koneksi browser yang masuk dan koneksi keluar tidak memiliki semantik lifecycle yang identik. Outbound WebSocket aktif menghalangi perilaku hibernation normal karena instance JavaScript ikut terlibat dalam koneksi eksternal yang masih hidup.

Batas ini penting untuk gateway, market-data relay, dan protocol bridge. Desain yang hemat untuk ribuan inbound socket yang idle dapat berperilaku sangat berbeda ketika setiap object juga mempertahankan outbound socket.

Pilih batas berdasarkan model state

WebSocket langsung pada Worker cocok ketika koneksi bersifat mandiri dan tidak membutuhkan pemegang shared state yang authoritative.

Durable Object berguna ketika key stabil seperti room ID, document ID, device ID, atau session ID harus memetakan banyak event dan client ke satu titik koordinasi.

Hibernation API menjadi penting ketika WebSocket pada Durable Object tersebut dapat idle dalam waktu yang berarti.

Keputusannya bersifat struktural:

Does the connection need shared coordination?
    |
    +-- no --> Worker WebSocket can be enough
    |
    +-- yes --> Durable Object
                  |
                  v
       Can connections be idle?
                  |
                  +-- yes --> Hibernation API
                  |
                  +-- no --> active lifecycle may be acceptable

Dukungan WebSocket di Cloudflare Workers bukan kemampuan biner. Batas upgrade, koordinasi, dan umur runtime merupakan tiga hal berbeda. Memisahkannya mencegah kode yang seharusnya hanya menangani satu koneksi berubah menjadi distributed state system tanpa desain yang jelas, sekaligus mencegah socket jangka panjang yang idle menahan instance JavaScript di memory tanpa kebutuhan.