Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Hibernation Integration

Hibernation integration lets oRPC leverage Hibernation APIs like Cloudflare’s Hibernation WebSocket, so your server can sleep between events without dropping active connections.

Installation

npm install @orpc/hibernation@beta
pnpm add @orpc/hibernation@beta
yarn add @orpc/hibernation@beta
bun add @orpc/hibernation@beta

Setup

import { HibernationHandlerPlugin } from '@orpc/hibernation'

const handler = new RPCHandler(router, {
  plugins: [
    new HibernationHandlerPlugin(),
  ],
})

Usage

The plugin provides HibernationAsyncIteratorClass and encodeHibernationRPCEvent to help you return an Async Iterator Object that utilizes the Hibernation APIs.

  1. Return a HibernationAsyncIteratorClass from your handler

    import { HibernationAsyncIteratorClass } from '@orpc/hibernation'
    
    const base = os.$context<{ ws: WebSocket }>()
    
    export const onMessage = base.handler(async ({ context }) => {
      return new HibernationAsyncIteratorClass<{ message: string }>((id) => {
        // Save the ID. You'll need it to send events later.
        context.ws.serializeAttachment({ id })
      })
    })
  2. Send events to clients with encodeHibernationRPCEvent

    import { encodeHibernationRPCEvent } from '@orpc/hibernation'
    import * as z from 'zod'
    
    const base = os.$context<{ getWebSockets: () => WebSocket[] }>()
    
    export const sendMessage = base
      .input(z.object({ message: z.string() }))
      .handler(async ({ input, context }) => {
        const websockets = context.getWebSockets()
    
        for (const ws of websockets) {
          const { id } = ws.deserializeAttachment()
    
          // yield an event to all clients
          ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, {
            // override the default RPC serializer if needed
            serializer: new RPCSerializer(),
          }))
          // return an event and stop the iterator
          ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, { event: 'close' }))
          // throw an error and stop the iterator
          ws.send(await encodeHibernationRPCEvent(id, new ORPCError('INTERNAL_SERVER_ERROR'), { event: 'error' }))
        }
      })
Cloudflare Durable Object Chat Room Example?

This example shows how to build a chat room with Cloudflare Durable Objects and WebSocket Hibernation. Everyone connected to the same Durable Object can exchange messages. You can try a working version in the Cloudflare Playground, see Playgrounds.

import { RPCHandler } from '@orpc/server/websocket'
import {
  encodeHibernationRPCEvent,
  HibernationAsyncIteratorClass,
  HibernationHandlerPlugin,
} from '@orpc/hibernation'
import { onError, os } from '@orpc/server'
import { DurableObject } from 'cloudflare:workers'
import * as z from 'zod'

const base = os.$context<{
  handler: RPCHandler<any>
  ws: WebSocket
  getWebsockets: () => WebSocket[]
}>()

export const router = {
  send: base.input(z.object({ message: z.string() })).handler(async ({ input, context }) => {
    const websockets = context.getWebsockets()

    for (const ws of websockets) {
      const data = ws.deserializeAttachment()
      if (typeof data !== 'object' || data === null) {
        continue
      }

      const { id } = data

      ws.send(await encodeHibernationRPCEvent(id, input.message))
    }
  }),
  onMessage: base.handler(async ({ context }) => {
    return new HibernationAsyncIteratorClass<string>((id) => {
      context.ws.serializeAttachment({ id })
    })
  }),
}

const handler = new RPCHandler(router, {
  interceptors: [
    onError((error) => {
      console.error(error)
    }),
  ],
  plugins: [
    new HibernationHandlerPlugin(),
  ],
})

export class ChatRoom extends DurableObject {
  async fetch(): Promise<Response> {
    const { '0': client, '1': server } = new WebSocketPair()

    this.ctx.acceptWebSocket(server)

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

  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
    await handler.message(ws, message, {
      context: {
        handler,
        ws,
        getWebsockets: () => this.ctx.getWebSockets(),
      },
    })
  }

  async webSocketClose(ws: WebSocket): Promise<void> {
    await handler.close(ws)
  }
}
import { RPCLink } from '@orpc/client/websocket'
import { createORPCClient } from '@orpc/client'
import type { router } from '../../worker/dos/chat-room'
import type { RouterClient } from '@orpc/server'

const websocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/chat-room`)

websocket.addEventListener('error', (event) => {
  console.error(event)
})

const link = new RPCLink({
  connect: () => websocket,
})

export const chatRoomClient: RouterClient<typeof router> = createORPCClient(link)
import { useEffect, useState } from 'react'
import { chatRoomClient } from '../lib/chat-room'

export function ChatRoom() {
  const [messages, setMessages] = useState<string[]>([])

  useEffect(() => {
    const controller = new AbortController()

    void (async () => {
      for await (const message of await chatRoomClient.onMessage(undefined, { signal: controller.signal })) {
        setMessages(messages => [...messages, message])
      }
    })()

    return () => {
      controller.abort()
    }
  }, [])

  const sendMessage = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const form = new FormData(e.target as HTMLFormElement)
    const message = form.get('message') as string

    await chatRoomClient.send({ message })
  }

  return (
    <div>
      <h1>Chat Room</h1>
      <p>Open multiple tabs to chat together</p>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <form onSubmit={sendMessage}>
        <input name="message" type="text" required defaultValue="hello" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

Last updated on August 6, 2026

Was this page helpful?