Skip to content

Commit 72baf97

Browse files
committed
chore(blog): add live ws migration (#3536)
1 parent 1137d33 commit 72baf97

File tree

2 files changed

+189
-0
lines changed
  • website/src/posts/2025-11-24-introducing-live-websocket-migration-hibernation

2 files changed

+189
-0
lines changed
303 KB
Loading
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)