|
| 1 | +export const author = "nathan-flurry" |
| 2 | +export const published = "2025-11-24" |
| 3 | +export const category = "changelog" |
| 4 | +export const keywords = ["websocket","realtime","actions","events","hibernating-websockets","sleep","fault-tolerance"] |
| 5 | + |
| 6 | +# Introducing Live WebSocket Migration and Hibernation |
| 7 | + |
| 8 | +Rivet now supports keeping WebSocket connections alive while actors upgrade, migrate, crash, or sleep. This eliminates many of the biggest pain points of building realtime applications with WebSockets. |
| 9 | + |
| 10 | +This is fully W3C compliant and requires no custom API changes to integrate. |
| 11 | + |
| 12 | +## Why WebSockets Have Historically Been Difficult |
| 13 | + |
| 14 | +Traditionally, applications opt to use stateless HTTP requests over WebSockets because: |
| 15 | + |
| 16 | +- **Expensive at scale**: It's expensive to keep WebSockets open for a high number of concurrent users |
| 17 | +- **Disruptive upgrades**: Application upgrades interrupt user experience because of WebSocket disconnects |
| 18 | +- **Difficult to rebalance load**: You can't rebalance active WebSockets to different machines when receiving a large influx of traffic |
| 19 | +- **High blast radius**: Application crashes disconnect all WebSockets |
| 20 | + |
| 21 | +## Introducing Live WebSocket Migration & Hibernation |
| 22 | + |
| 23 | +We set out to solve this by enabling WebSockets to Rivet Actors to stay open while the actor upgrades, migrates, crashes, or goes to sleep. |
| 24 | + |
| 25 | +This comes with a series of benefits that previously were only possible using stateless HTTP requests: |
| 26 | + |
| 27 | +- **Idle WebSockets require no compute**: Actors can go to sleep while leaving client WebSockets open, meaning you no longer have to pay for active compute resources. Actors automatically wake up when a message is received or the connection closes. |
| 28 | +- **Upgrade your application without terminating WebSockets**: Applications can be upgraded without terminating WebSocket connections by automatically migrating the WebSocket connection to the new version of the actor |
| 29 | +- **Load rebalancing**: Load is better distributed when scaling up your application since actors can reschedule to machines with less load |
| 30 | +- **Resilience**: Application crashes from errors, hardware faults, or network faults no longer interrupt WebSockets. Instead, the actor will immediately reschedule and the WebSocket will continue to operate as if nothing happened |
| 31 | + |
| 32 | +## Show Me the Code |
| 33 | + |
| 34 | +### Actions & Events API |
| 35 | + |
| 36 | +If using the actions & events API, upgrade to Rivet v2.0.24 and it will work out of the box. |
| 37 | + |
| 38 | +The following code will automatically use WebSocket hibernation. When users are sitting idle connected to the chat room, the actor can go to sleep while still keeping the WebSocket open, ready to send actions or receive events: |
| 39 | + |
| 40 | +<CodeGroup> |
| 41 | +```typescript {{"title":"Actor"}} |
| 42 | +import { actor } from "rivetkit"; |
| 43 | + |
| 44 | +interface Message { |
| 45 | + id: string; |
| 46 | + username: string; |
| 47 | + text: string; |
| 48 | + timestamp: number; |
| 49 | +} |
| 50 | + |
| 51 | +interface State { |
| 52 | + messages: Message[]; |
| 53 | +} |
| 54 | + |
| 55 | +const chatRoom = actor({ |
| 56 | + state: { messages: [] } as State, |
| 57 | + |
| 58 | + actions: { |
| 59 | + setUsername: (c, username: string) => { |
| 60 | + c.conn.state.username = username; |
| 61 | + }, |
| 62 | + |
| 63 | + sendMessage: (c, text: string) => { |
| 64 | + const message = { |
| 65 | + id: crypto.randomUUID(), |
| 66 | + username: c.conn.state.username, |
| 67 | + text, |
| 68 | + timestamp: Date.now() |
| 69 | + }; |
| 70 | + |
| 71 | + c.state.messages.push(message); |
| 72 | + |
| 73 | + // Broadcast to all connected clients |
| 74 | + c.broadcast("messageReceived", message); |
| 75 | + |
| 76 | + return message; |
| 77 | + }, |
| 78 | + |
| 79 | + getHistory: (c) => { |
| 80 | + return c.state.messages; |
| 81 | + } |
| 82 | + } |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +```typescript {{"title":"Client"}} |
| 87 | +import { createClient } from "rivetkit/client"; |
| 88 | +import type { registry } from "./registry"; |
| 89 | + |
| 90 | +const client = createClient<typeof registry>("http://localhost:8080"); |
| 91 | + |
| 92 | +// Connect to the chat room |
| 93 | +const chatRoom = client.chatRoom.getOrCreate("general"); |
| 94 | +const connection = chatRoom.connect(); |
| 95 | + |
| 96 | +// Listen for new messages |
| 97 | +connection.on("messageReceived", (message) => { |
| 98 | + console.log(`${message.username}: ${message.text}`); |
| 99 | +}); |
| 100 | + |
| 101 | +// Set username (stored in per-connection state) |
| 102 | +await connection.setUsername("alice"); |
| 103 | + |
| 104 | +// Send a message |
| 105 | +await connection.sendMessage("Hello everyone!"); |
| 106 | +``` |
| 107 | +</CodeGroup> |
| 108 | + |
| 109 | +Read more about [actions](/docs/actors/actions) and [events](/docs/actors/events). |
| 110 | + |
| 111 | +### Low-Level WebSocket API |
| 112 | + |
| 113 | +The low-level WebSocket API (`onWebSocket`) can opt in to WebSocket hibernation starting in Rivet v2.0.24. Configure `options.canHibernateWebSocket` with either `true` or a conditional closure based on the request (`(request) => boolean`). |
| 114 | + |
| 115 | +The `open`, `message`, and `close` events fire as they normally would on a WebSocket. When the actor migrates to a separate machine, `c.conn.state` is persisted and no new `open` event is triggered. |
| 116 | + |
| 117 | +For example: |
| 118 | + |
| 119 | +<CodeGroup> |
| 120 | +```typescript {{"title":"Actor"}} |
| 121 | +import { actor } from "rivetkit"; |
| 122 | + |
| 123 | +interface State { |
| 124 | + messages: string[]; |
| 125 | +} |
| 126 | + |
| 127 | +const chatRoom = actor({ |
| 128 | + state: { messages: [] } as State, |
| 129 | + |
| 130 | + options: { |
| 131 | + canHibernateWebSocket: true |
| 132 | + }, |
| 133 | + |
| 134 | + onWebSocket: (c, websocket) => { |
| 135 | + websocket.addEventListener("open", () => { |
| 136 | + // Send existing messages to new connection |
| 137 | + websocket.send(JSON.stringify({ |
| 138 | + type: "history", |
| 139 | + messages: c.state.messages |
| 140 | + })); |
| 141 | + }); |
| 142 | + |
| 143 | + websocket.addEventListener("message", (event) => { |
| 144 | + const data = JSON.parse(event.data); |
| 145 | + |
| 146 | + if (data.type === "setUsername") { |
| 147 | + // Store username in per-connection state (persists across sleep cycles) |
| 148 | + c.conn.state.username = data.username; |
| 149 | + return; |
| 150 | + } |
| 151 | + |
| 152 | + if (data.type === "message") { |
| 153 | + // Store and broadcast the message with username from connection state |
| 154 | + const message = `${c.conn.state.username}: ${data.text}`; |
| 155 | + c.state.messages.push(message); |
| 156 | + websocket.send(message); |
| 157 | + c.saveState(); |
| 158 | + } |
| 159 | + }); |
| 160 | + } |
| 161 | +}); |
| 162 | +``` |
| 163 | + |
| 164 | +```typescript {{"title":"Client"}} |
| 165 | +import { createClient } from "rivetkit/client"; |
| 166 | +import type { registry } from "./registry"; |
| 167 | + |
| 168 | +const client = createClient<typeof registry>("http://localhost:8080"); |
| 169 | + |
| 170 | +// Get the chat room actor |
| 171 | +const chatRoom = client.chatRoom.getOrCreate("general"); |
| 172 | + |
| 173 | +// Open a WebSocket connection |
| 174 | +const ws = await chatRoom.websocket("/"); |
| 175 | + |
| 176 | +// Listen for messages |
| 177 | +ws.addEventListener("message", (event) => { |
| 178 | + console.log("Received:", event.data); |
| 179 | +}); |
| 180 | + |
| 181 | +// Set username (stored in per-connection state) |
| 182 | +ws.send(JSON.stringify({ type: "setUsername", username: "alice" })); |
| 183 | + |
| 184 | +// Send a message |
| 185 | +ws.send(JSON.stringify({ type: "message", text: "Hello from WebSocket!" })); |
| 186 | +``` |
| 187 | +</CodeGroup> |
| 188 | + |
| 189 | +Read more about the [low-level WebSocket API](/docs/actors/websocket-handler). |
0 commit comments