@sigx/actors-tcp · Preview

TCP#

One multiplexed, framed connection per peer instead of one HTTP connection per in-flight request. On Node, this is the recommended host-to-host transport.

v0.11.0 MIT

Installation#

Terminal
pnpm add @sigx/actors-tcp
TypeScript
import { cluster, httpTransport } from '@sigx/actors/cluster';
import { tcpTransport } from '@sigx/actors-tcp';

cluster({
    providers,
    advertise: 'http://10.0.4.7:7311',
    secret: process.env.HOST_SECRET,
    // A CHAIN: TCP wherever the peer advertises it, HTTP everywhere else.
    // That is what makes a rolling deploy of this transport possible.
    transport: [tcpTransport({ port: 11111 }), httpTransport()],
});

Why you would use it#

On Node this is the transport hosts should run on — for latency and CPU per hop as much as for socket count.

Per hop, one machine, five runs, against HTTP with its pool tuned to the concurrency:

Socketscalls/sp99 per call
tuned HTTP65~15.6k10–13 ms
TCP3~91k1.3 ms

Across a sixteen-host fleet under an engine-bound workflow load, TCP completed 1.4–1.9× the runs per second past 200 offered, with start-latency p99 under 200 ms where HTTP's was over 100 s — HTTP's per-peer fetch pool saturated, calls timed out, and retries duplicated directory claims (10,184 dispatch retries against 16; 16,024 claim conflicts against 16).

Socket count is the other reason. HTTP's pool sizes to concurrency × peers: roughly 12,600 sockets per host at concurrency 64 across 99 peers — file descriptors, kernel buffers, conntrack entries and a connection burst on every peer restart. One connection per peer does not change with RTT.

The full numbers are on Host transports.

HTTP remains the default#

And must: @sigx/actors/cluster stays zero-dependency and WinterCG-clean so Cloudflare Workers keep working, and HTTP is the only transport that runs everywhere. It stays the client endpoint and the fallback the chain drops to during a rolling deploy.

This package is Node-only by design — it uses node:net.

Deploying it#

HostDescriptor.addresses carries a tcp entry per host, so a mixed cluster is expressible and the rollout is safe:

  1. Deploy with transport: [tcpTransport(), httpTransport()] everywhere.
  2. Hosts on the new build advertise tcp and use it with each other; hosts that do not are still reached over HTTP.
  3. Once every host advertises tcp, drop httpTransport() from the chain if you want the internal HTTP mount gone entirely.

When a connection drops#

A unary call already on the wire when its connection drops rejects with an ordinary Error — [sigx actors] host connection closed with this call in flight (<reason>): outcome unknown, not retried — never unreachable, because the owner may have run it. Re-issue it only if the method is idempotent; see when a call is retried. Streams and watches still fail unreachable before their first chunk.

Two hosts dialling each other at once keep both sides' in-flight calls: the losing socket drains and closes once both ends are done. handshakeTimeoutMs bounds an outbound connect as well as an inbound handshake.

Next steps#