Websocket connection issues on cloudflare workers with @rivetkit/core@0.9.0

I recently used actor-core@0.8.0 to build a game server but had issues with actor-actor communication which stalled deployment. Since the release of @rivetkit/core@0.9.0, I've migrated the game server to the new API and verified that actor-actor communication works fine. I proxied an RPC request from one actor to another to verify this. The issue now is websocket connections don't work anymore. I've looked through the and still can't figure out what is causing the issue. I also cloned the repo to run the cloudflare examples locally, same thing. On the client I get the below log. msg="socket closed" code=1002 reason="Mismatch client protocol" wasClean=true On the server I get: level=WARN msg="websocket closed" code=1006 reason="WebSocket disconnected without sending Close frame." wasClean=false Below are the details of the closevent from https://github.com/rivet-gg/rivetkit/blob/feeaadcf1a2de5c271d16022cb8fd74ff6c31ad1/packages/core/src/actor/router-endpoints.ts#L160
CloseEvent {
wasClean: false,
reason: 'WebSocket disconnected without sending Close frame.',
code: 1006,
type: 'close',
eventPhase: 2,
composed: false,
bubbles: false,
cancelable: false,
defaultPrevented: false,
returnValue: true,
currentTarget: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
target: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
srcElement: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
timeStamp: 0,
isTrusted: true,
cancelBubble: false,
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3
}
CloseEvent {
wasClean: false,
reason: 'WebSocket disconnected without sending Close frame.',
code: 1006,
type: 'close',
eventPhase: 2,
composed: false,
bubbles: false,
cancelable: false,
defaultPrevented: false,
returnValue: true,
currentTarget: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
target: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
srcElement: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
timeStamp: 0,
isTrusted: true,
cancelBubble: false,
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3
}
I'm not sure what is causing the issues and any pointers will be really helpful. PS: I've also tried SSE and it doesn't receive any broadcast from the actors.
GitHub
rivetkit/packages/core/src/actor/router-endpoints.ts at feeaadcf1a2...
🎭 Stateful Serverless That Runs Anywhere. The easiest way to build stateful, AI agent, collaborative, or local-first applications. Deploy to Rivet, Cloudflare, Bun, Node.js, and more. - rivet-gg/ri...
35 Replies
Emmanuel
EmmanuelOPβ€’6mo ago
I've also explored https://github.com/rivet-gg/rivetkit/blob/314843828b8be1e51d9dd52b5c84b4af4abc533d/packages/platforms/cloudflare-workers/src/manager-driver.ts#L94 and all hacks related to cloudflare and still been unable to get it to work. I use node 24 and pnpm 10.
GitHub
rivetkit/packages/platforms/cloudflare-workers/src/manager-driver.t...
🧰 Lightweight libraries for backends. Install one package, scale to production. Just a library, no SaaS. - rivet-gg/rivetkit
Nathan
Nathanβ€’6mo ago
hey! it's late here, i'll give this a look in the morning. this should be covered by our test cases, but i'll double check it's doing exactly what you described. can you share a code snippet of the relevant parts of the code? haha you really went deep
Emmanuel
EmmanuelOPβ€’6mo ago
haha yes, I've spent about 3 weeks building a game server and found rivet and had it done in about a day. This is my new favourite library πŸ™Œ . Yes, I just modified the cloudflare-workers example to publish the state when increment is called.
// registry.ts
import { actor, setup } from "@rivetkit/actor";

export const counter = actor({
onAuth: () => {
// Configure auth here
},
state: { count: 0 },
actions: {
increment: (c, x: number) => {
c.state.count += x;
c.broadcast("newCount", c.state.count);
return c.state.count;
},
},
});

export const registry = setup({
use: { counter },
});
// registry.ts
import { actor, setup } from "@rivetkit/actor";

export const counter = actor({
onAuth: () => {
// Configure auth here
},
state: { count: 0 },
actions: {
increment: (c, x: number) => {
c.state.count += x;
c.broadcast("newCount", c.state.count);
return c.state.count;
},
},
});

export const registry = setup({
use: { counter },
});
//client.ts

import { createClient } from "@rivetkit/actor/client";
import type { registry } from "../src/registry";

// Create RivetKit client
const client = createClient<typeof registry>(
process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787",
);

async function main() {
console.log("πŸš€ Cloudflare Workers Client Demo");

try {
// Create counter instance
const counter = client.counter.getOrCreate("demo");
const conn = counter.connect();

conn.on("newCount", (count: number) => console.log("Event:", count));
// Increment counter
console.log("Incrementing counter 'demo'...");
const result1 = await counter.increment(1);
console.log("New count:", result1);

// Increment again with larger value
console.log("Incrementing counter 'demo' by 5...");
const result2 = await counter.increment(5);
console.log("New count:", result2);

// Create another counter
const counter2 = client.counter.getOrCreate("another");
console.log("Incrementing counter 'another' by 10...");
const result3 = await counter2.increment(10);
console.log("New count:", result3);

console.log("βœ… Demo completed!");
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
}
}

main().catch(console.error);
//client.ts

import { createClient } from "@rivetkit/actor/client";
import type { registry } from "../src/registry";

// Create RivetKit client
const client = createClient<typeof registry>(
process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787",
);

async function main() {
console.log("πŸš€ Cloudflare Workers Client Demo");

try {
// Create counter instance
const counter = client.counter.getOrCreate("demo");
const conn = counter.connect();

conn.on("newCount", (count: number) => console.log("Event:", count));
// Increment counter
console.log("Incrementing counter 'demo'...");
const result1 = await counter.increment(1);
console.log("New count:", result1);

// Increment again with larger value
console.log("Incrementing counter 'demo' by 5...");
const result2 = await counter.increment(5);
console.log("New count:", result2);

// Create another counter
const counter2 = client.counter.getOrCreate("another");
console.log("Incrementing counter 'another' by 10...");
const result3 = await counter2.increment(10);
console.log("New count:", result3);

console.log("βœ… Demo completed!");
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
}
}

main().catch(console.error);
Nathan
Nathanβ€’6mo ago
aye that's awesome! where did you find us? let me give this a go in the morning 🫑
Emmanuel
EmmanuelOPβ€’6mo ago
Great. Thanks. I'll keep an eye out. Open alternative https://openalternative.co/rivet. The quickstart took about 5 minutes to get through and deploy. It is an amazing framework that has already highlighted multiple use cases for the platform I'm working on.
Nathan
Nathanβ€’6mo ago
didn't get to the bottom of this today, currently top of the todo list
Emmanuel
EmmanuelOPβ€’6mo ago
Great thanks for looking into it. Any pointers on what I should look at in the codebase or a design doc that can guide me? Because it works on actor-core and just fails with actor-actor comms.
Nathan
Nathanβ€’6mo ago
there’s a file called something like β€œhttp client driver.” i’m afk so i can’t find it quickly. we migrated to pnpm right before we launched which broke the cf test suite, so it’s unfortunately probably not the quickest thing to get back up. if you just need to test locally β€” id recommend just using nodejs for a minute while i get this fixed up.
Emmanuel
EmmanuelOPβ€’6mo ago
Thanks, I'll look through the files and follow the failed tests. Unfortunately, I need to test on cloudflare
Nathan
Nathanβ€’6mo ago
looking now
Emmanuel
EmmanuelOPβ€’6mo ago
πŸ™Œ
Nathan
Nathanβ€’6mo ago
thanks for reporting! if you're able to keep a running list of any pain points, annoyances, or feature requests as you work with rivetkit – it'd be incredibly helpful
Emmanuel
EmmanuelOPβ€’6mo ago
Thanks for the quick fix. I'll look through the PR to see what caused the issue. Definitely, I'll share what I find. A bit strange. I just had a look at your PR. I applied a patch to fix the missing return when I followed the logs which fixed the below error. Before the patch, these were the error logs:
// Server logs
level=INFO msg="internal error" code=internal_error message="Context is not finalized. Did you forget to return a Response object or `await next()`?" method=GET path=/connect/websocket
// Server logs
level=INFO msg="internal error" code=internal_error message="Context is not finalized. Did you forget to return a Response object or `await next()`?" method=GET path=/connect/websocket
// client logs

level=WARN msg="socket closed" code=1002 reason="Expected 101 status code" wasClean=false
level=WARN msg="failed to reconnect" attempt=10 error="Error: Closed"
// client logs

level=WARN msg="socket closed" code=1002 reason="Expected 101 status code" wasClean=false
level=WARN msg="failed to reconnect" attempt=10 error="Error: Closed"
Then after the patch, the error changed and the logs from the connection error became,
// server

level=WARN msg="websocket closed" code=1006 reason="WebSocket disconnected without sending Close frame." wasClean=false
// server

level=WARN msg="websocket closed" code=1006 reason="WebSocket disconnected without sending Close frame." wasClean=false
// client
level=INFO msg="socket closed" code=1002 reason="Mismatch client protocol" wasClean=true
level=WARN msg="failed to reconnect" attempt=7 error="Error: Closed"
// client
level=INFO msg="socket closed" code=1002 reason="Mismatch client protocol" wasClean=true
level=WARN msg="failed to reconnect" attempt=7 error="Error: Closed"
So the websocket connection still fails. Did you run the client with a websocket connection? Another error shows up when you try to connect from the client.
Nathan
Nathanβ€’6mo ago
i ran my demo with a ws connection, let me try your code
Nathan
Nathanβ€’6mo ago
that's odd, it works for me with https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/actor@1060
No description
Nathan
Nathanβ€’6mo ago
can you send your index.ts?
Emmanuel
EmmanuelOPβ€’6mo ago
import { createServerHandler } from "@rivetkit/cloudflare-workers"; import { registry } from "./registry"; const { handler, ActorHandler } = createServerHandler(registry); export { handler as default, ActorHandler }; I didn't make any changes to index.ts.
Nathan
Nathanβ€’6mo ago
roger, i'll try with a fresh project in a moment and send over a zip if it works
Emmanuel
EmmanuelOPβ€’6mo ago
I just pulled your branch and added the broadcast. I'm still getting same error πŸ₯²
No description
Nathan
Nathanβ€’6mo ago
oh boy, this is a spicy one are you on windows by chance? + what package manager & what command are you using to run the server? curious if your machine is using a diff version of wrangler. i can reproduce part of the error after upgrading wrangler. give me a minute.
Emmanuel
EmmanuelOPβ€’6mo ago
I'm on a mac m2 There's no local version of wrangler installed so the npm version is what is being used. Package manager pnpm v10, node v24
Nathan
Nathanβ€’6mo ago
this is mad haha i'm on linux, let me try running this locally on my mac. i'll push another branch in a second if you'd be kind enough to test again
Nathan
Nathanβ€’6mo ago
ah i see the issue here – try this client:
import { createClient } from "@rivetkit/actor/client";
import type { registry } from "../src/registry";

// Create RivetKit client
const client = createClient<typeof registry>(
process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787",
);

async function main() {
console.log("πŸš€ Cloudflare Workers Client Demo");

try {
// Create counter instance
const counter = client.counter.getOrCreate("demo").connect();
// await counter.resolve();
console.log("connect");
counter.on("foo", (x) => console.log("=== output ===", x));
// await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for connection

// Increment counter
console.log("Incrementing counter 'demo'...");
const result1 = await counter.increment(1);
console.log("New count:", result1);

// Increment again with larger value
console.log("Incrementing counter 'demo' by 5...");
const result2 = await counter.increment(5);
console.log("New count:", result2);

// Create another counter
const counter2 = client.counter.getOrCreate("another");
console.log("Incrementing counter 'another' by 10...");
const result3 = await counter2.increment(10);
console.log("New count:", result3);

console.log("βœ… Demo completed!");
setTimeout(() => {}, 1000);
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
}
}

main().catch(console.error);
import { createClient } from "@rivetkit/actor/client";
import type { registry } from "../src/registry";

// Create RivetKit client
const client = createClient<typeof registry>(
process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787",
);

async function main() {
console.log("πŸš€ Cloudflare Workers Client Demo");

try {
// Create counter instance
const counter = client.counter.getOrCreate("demo").connect();
// await counter.resolve();
console.log("connect");
counter.on("foo", (x) => console.log("=== output ===", x));
// await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for connection

// Increment counter
console.log("Incrementing counter 'demo'...");
const result1 = await counter.increment(1);
console.log("New count:", result1);

// Increment again with larger value
console.log("Incrementing counter 'demo' by 5...");
const result2 = await counter.increment(5);
console.log("New count:", result2);

// Create another counter
const counter2 = client.counter.getOrCreate("another");
console.log("Incrementing counter 'another' by 10...");
const result3 = await counter2.increment(10);
console.log("New count:", result3);

console.log("βœ… Demo completed!");
setTimeout(() => {}, 1000);
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
}
}

main().catch(console.error);
working on updating docs & logging a warning to prevent this error: https://github.com/rivet-gg/rivetkit/issues/1061
GitHub
chore: log warning if mixing using handler & connection Β· Issue #1...
Mixing using the actor handle and actor connection can cause some unexpected behaviors in terms of race conditions. Update the actor client &amp; handle: Log warning if calling stateless rpcs on a ...
Emmanuel
EmmanuelOPβ€’6mo ago
It does work from the browser now but still failing with the sample you sent.
Nathan
Nathanβ€’6mo ago
Can you send the full logs of the error? The screenshot above is clipped.
Emmanuel
EmmanuelOPβ€’6mo ago
Sure, I'll rerun it and send it over today.
Emmanuel
EmmanuelOPβ€’6mo ago
No description
Emmanuel
EmmanuelOPβ€’6mo ago
import { createClient } from "@rivetkit/actor/client"; import type { registry } from "../src/registry"; // Create RivetKit client const client = createClient<typeof registry>( process.env.RIVETKIT_ENDPOINT ?? "http://localhost:8787", ); async function main() { console.log("πŸš€ Cloudflare Workers Client Demo"); try { // Create counter instance const counter = client.counter.getOrCreate("demo").connect(); console.log("connect"); counter.on("increment", console.log); counter.on("foo", (x) => console.log("=== output ===", x)); // await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for connection // Increment counter console.log("Incrementing counter 'demo'..."); const result1 = await counter.increment(1); console.log("New count:", result1); // Increment again with larger value console.log("Incrementing counter 'demo' by 5..."); const result2 = await counter.increment(5); console.log("New count:", result2); // Create another counter const counter2 = client.counter.getOrCreate("another"); console.log("Incrementing counter 'another' by 10..."); const result3 = await counter2.increment(10); console.log("New count:", result3); console.log("βœ… Demo completed!"); setTimeout(() => {}, 1000); } catch (error) { console.error("❌ Error:", error); process.exit(1); } } main().catch(console.error); // client code
Nathan
Nathanβ€’6mo ago
any idea where "user" is coming from? it's not in your code
No description
Emmanuel
EmmanuelOPβ€’6mo ago
I have a full setup in react. The frontend was still running and tried to connect to the server. DO you think that is causing any issues?
Nathan
Nathanβ€’6mo ago
ah gotcha. i don’t think so, i was just confused by the logs just noticed you’re using bun to run the test script. bun has a different websocket implementation than node, i’ll see if i can reproduce it with that later today.
Emmanuel
EmmanuelOPβ€’6mo ago
Ok, I'll spin it up with node. That probably explains why it works in the browser.
Nathan
Nathanβ€’6mo ago
thanks for taking the time to help get this fixed – let me know if you run in to any more issues or have feedback!

Did you find this page helpful?