WebSocket
The same frames as TCP, over one port — the one your proxy already forwards.
Installation
pnpm add @sigx/actors-ws wsimport { createServer } from 'node:http';
import { defineActorApp } from '@sigx/actors/host';
import { createAppHandler } from '@sigx/actors/node';
import { cluster, httpTransport } from '@sigx/actors/cluster';
import { wsTransport } from '@sigx/actors-ws';
const ws = wsTransport({
advertiseUrl: () => `ws://10.0.4.7:7311/_sigx/host-ws`,
});
const app = defineActorApp({ actors, storage }).use(
cluster({ providers, advertise: 'http://10.0.4.7:7311', secret, transport: [ws, httpTransport()] }),
);
const server = createServer(createAppHandler(app));
await ws.attach(server); // the upgrade a route cannot express
await new Promise<void>((r) => server.listen(7311, r));
await app.start();
Why this and not TCP
Raw TCP is the better wire where you control the network. WebSocket earns its place in the cases TCP cannot reach:
- One port. Frames ride the HTTP listener the host already has — no second port in a security group, a Service, or a firewall rule.
- Through proxies and load balancers. They forward WebSocket; they will generally not forward an arbitrary TCP protocol.
- Dialable from WinterCG runtimes, since the client half is the standard
WebSocket.node:netcan never be.
The trade is a small framing tax over raw TCP, and a ws peer dependency for the Node server
half.
Measured
At concurrency 64 against a tuned HTTP baseline:
| Connections per peer | ops/s | p99 | bytes/call | |
|---|---|---|---|---|
| tuned HTTP | 64 | 14,287 | 9.6 ms | 640 |
| WebSocket | 1 | 63,495 | 1.58 ms | 236 |
Read the two wins differently: the socket count holds at any RTT, while the 4.4× throughput is a software ratio a real network largely absorbs — ~70µs versus ~16µs per call, against a LAN round trip of 200–1000µs.
HTTP remains the default and must, so Workers keep working.
Why attach() exists
ActorRoute.handle returns a Response and cannot express a Node WebSocket upgrade — that
needs the raw socket. So this is the one piece the plugin's route seam cannot carry, and the
package deliberately does not own your server: you hand it yours.
Next steps
- Installation — mounting and options.
- API reference — exports.
- Host transports — the full comparison.
