callwire 2.0.4

High-performance bidirectional RPC over TCP with MessagePack framing — Go, Python, Rust interop
Documentation

Callwire

High-performance, bidirectional RPC across Go, Python, Rust, and TypeScript — over raw TCP with MessagePack framing.

No schemas. No .proto files. No codegen. Export a function, call it from anywhere.


Features

  • Zero-schema RPC — export any function, call it from any language
  • Bidirectional — clients and servers can call each other over the same socket
  • v2 Orchestration — one callwire.toml spawns and connects workers automatically
  • Dynamic routing — connect to a registry, call any function without knowing worker addresses
  • TLS & mTLS — secure transport with optional client certificate auth
  • Batch API — fire multiple calls concurrently over a single connection
  • Streaming — server-side streaming via generators / AsyncIterable
  • Auto-reconnect — exponential backoff on connection drops

Quick Start

Go

import "github.com/emaad/callwire"

// Export a function
callwire.Export("add", func(a, b int) int { return a + b })

// Call a remote function
client, _ := callwire.Connect("localhost:9090")
result, _ := callwire.Ref[int](client, "add")(10, 20) // 30

Python

import callwire

# 1. Export a local function (makes it server-ready)
@callwire.export
def add(a, b):
    return a + b

# 2. Dynamic module import (connects & invokes dynamically)
from callwire import add

result = add(10, 20)  # 30

Rust

use callwire::{Client, register_unary};

register_unary("add", |(a, b): (i64, i64)| Ok(a + b));

let client = Client::connect("127.0.0.1:9090").await?;
let result: i64 = client.import("add", &(10i64, 20i64)).await?; // 30

TypeScript

import { Server, remote } from 'callwire';

// 1. Export local function
const server = new Server();
server.export('add', ([a, b]) => (a as number) + (b as number));
await server.serve('0.0.0.0', 9090);

// 2. Call dynamically using the remote Proxy
const result = await remote.add(10, 20); // 30

Installation & Publishing

  • npm: npm install -g @emaad-ansari/callwire (latest version 2.0.3)
  • cargo: cargo install callwire --version 2.0.3
  • pip: pip install callwire==2.0.3

These packages are automatically published on each release via the CI workflow.

Orchestration (v2)

Workers are auto-discovered by the callwire init CLI and declared in callwire.toml:

[project]
name = "my-project"
version = "1.0.0"

[services.go-worker]
dev_cmd  = "cd go/callwire && go run examples/server.go"
prod_cmd = "./bin/go-worker"

[services.rust-worker]
dev_cmd  = "cd rust && cargo run --quiet --example my-worker"
prod_cmd = "./bin/rust-worker"

Generate it with any of the four native CLIs — they all produce the same output:

# Python
PYTHONPATH=python python3 -m callwire init

# Go
cd go/callwire && go run ./cmd/callwire/ init

# Rust
cargo run --manifest-path rust/Cargo.toml --bin callwire -- init

# TypeScript
npx tsx ts/src/cli.ts init

Then call init() — Callwire starts a registry, spawns workers, and routes everything automatically:

import callwire

callwire.init()  # reads callwire.toml, spawns workers

# Import functions dynamically as if they were local!
from callwire import add, predict

res1 = add(15, 27)      # → routed to Go worker
res2 = predict("data")  # → routed to Rust worker

callwire.shutdown()

See the full demo → examples/2_orchestrated/demo.py

FastAPI integration

from contextlib import asynccontextmanager
from fastapi import FastAPI
import callwire

@asynccontextmanager
async def lifespan(app: FastAPI):
    await callwire.async_init()
    yield
    await callwire.async_shutdown()

app = FastAPI(lifespan=lifespan)

Service Discovery & Dynamic Routing

Workers self-register with the registry. Clients connect once and call anything dynamically — no worker addresses needed.

# Python — dynamic module import
from callwire import add
result = add(10, 20)  # routed transparently via registry
// Rust — connect to registry, route calls transparently
let client = callwire::Client::connect_registry("127.0.0.1:29000").await?;
let sum: i32 = client.import("add", &(10, 20)).await?;
// TypeScript — connect to registry, route calls transparently
const client = new Client();
await client.connectRegistry('127.0.0.1', 29000);
const sum = await client.call<number>('add', [10, 20]);

For load-balancing across multiple workers of the same type, use DiscoverPool:

pool, _ := callwire.NewDiscoverPool("127.0.0.1:29090", "my-service")
result, _ := callwire.DiscoverRef[string](pool, "say_hello")("World")

TLS & mTLS

// Go — TLS server
callwire.ServeWithTLS("0.0.0.0:9090", callwire.TLSConfig{
    CertPem: cert,
    KeyPem:  key,
})

// Go — TLS client (with optional mTLS)
client, _ := callwire.ConnectWithReconnectTLS("localhost:9090", callwire.TLSConfig{
    CAPem: caCert,
})
# Python — TLS client
client.connect("localhost", 9090, tls={
    "cafile":   "ca.pem",
    "certfile": "client.pem",  # mTLS
    "keyfile":  "client.key",  # mTLS
})
// Rust — TLS client
let client = callwire::TlsConfig { ca_pem: Some(ca_pem), ..Default::default() }
    .connect("127.0.0.1:9090").await?;
// TypeScript — TLS server
const server = new Server();
await server.serve('0.0.0.0', 9090, {
  cert: fs.readFileSync('server.pem', 'utf8'),
  key:  fs.readFileSync('server.key', 'utf8'),
});

// TypeScript — TLS client (skip verify for self-signed)
const client = new Client({ tls: { rejectUnauthorized: false } });
await client.connect('127.0.0.1', 9090);

// TypeScript — TLS client with CA verification + mTLS
const clientMTLS = new Client({ tls: {
  ca:   fs.readFileSync('ca.pem', 'utf8'),
  cert: fs.readFileSync('client.pem', 'utf8'),
  key:  fs.readFileSync('client.key', 'utf8'),
}});
await clientMTLS.connect('127.0.0.1', 9090);

Streaming

// TypeScript — server-side streaming
server.export('count_up', async function* ([n]) {
  for (let i = 1; i <= (n as number); i++) yield i;
});

for await (const chunk of client.callStream<number>('count_up', [5])) {
  console.log(chunk); // 1, 2, 3, 4, 5
}

Examples

examples/
├── 1_standalone/   — One Go server, one client (Python / Rust / TypeScript)
└── 2_orchestrated/ — One command spawns Go + Rust workers automatically

examples/README.md


Configuration

Env Var Default Description
CALLWIRE_HOST localhost Default hostname for auto-serving & clients
CALLWIRE_PORT 9090 Default port
CALLWIRE_AUTO 1 Set to 0 to disable auto-server on Export
CALLWIRE_REGISTRY (set by orchestrator) Registry address for worker mode
CALLWIRE_SPAWNED (set by orchestrator) 1 when running as a managed worker

Running Tests

# Go
cd go/callwire && go test -v ./...

# Python
cd python && .venv/bin/python3 -m unittest discover -s . -p "test_*.py"

# Rust
cd rust && cargo test -- --nocapture

# TypeScript
cd ts && npm test

Wire Protocol

Callwire uses a simple, fully-specified binary protocol — implement it in any language.
SPEC.md


Performance

~33 µs per round-trip · ~81K calls/sec on a single connection · 1.3–1.7× faster than gRPC for unary workloads on Apple M4.

Metric Callwire gRPC Δ
Latency — noop 32.7 µs 57.7 µs 1.76× faster
Latency — add(a, b) 34.6 µs 58.8 µs 1.70× faster
Throughput (10 workers) 80K calls/sec 49K calls/sec 1.65× faster
Throughput (100 workers) 81K calls/sec 62K calls/sec 1.30× faster

Full breakdown → benchmarks/compare_grpc.md


How It Compares

vs gRPC

Dimension Callwire gRPC
Schema None — export any function Required .proto files + codegen
Latency (noop) 32.7 µs 57.7 µs
Throughput 81K calls/sec 62K calls/sec
Transport Raw TCP (4-byte length + msgpack) HTTP/2 + HPACK
Bidirectional Same socket, any order HTTP/2 streams (half-duplex per stream)
Orchestration Built-in callwire.toml + init() External (Kubernetes, Consul, etc.)
Languages Go, Python, Rust, TypeScript 11+ languages
Streaming Server-side (generators) Unary + server + client + bidi
Browser No Yes (gRPC-Web)
Ecosystem Minimal Envoy, gRPC-Gateway, health probes, reflection

When to pick Callwire: services in supported languages, especially polyglot stacks (Go+Python+Rust+TS), where developer velocity matters more than formal API contracts.

When to pick gRPC: cross-org APIs, browser clients, languages Callwire doesn't support, existing gRPC infrastructure.

vs protosocket (Momento)

Rust-only TCP RPC framework (v1: 100KHz, sub-ms p99.9). Callwire has protosocket beat on language coverage (4 runtimes vs 1) and built-in orchestration. protosocket is faster per-core for pure Rust workloads and has production battle-testing at Momento scale.

vs ZeroRPC / Zero (zeroapi)

Python MessagePack-over-ZeroMQ RPC. Zero hits ~100K req/s on TCP but is Python-only and has a hard gevent dependency. Callwire matches that throughput in every language and adds TLS, streaming, orchestration, and cross-language interop.

vs MagicOnion (C#)

MessagePack-over-gRPC for .NET/Unity. Shares Callwire's zero-schema philosophy (C# interfaces instead of .proto) but is C#-only and inherits gRPC's HTTP/2 overhead. Callwire is 1.3–1.7× faster on wire latency and spans 4 runtimes.

vs Cap'n Proto RPC

Zero-copy RPC with time-travel (promise pipelining). Extremely fast deserialization, but requires .capnp schemas and supports only 6 languages. Callwire has no schema, wider language coverage, and built-in orchestration.

vs Apache Thrift

Mature, 20+ language RPC with multiple transports. Requires .thrift schemas + codegen, no streaming. Callwire is simpler to set up and faster for the languages it supports.

vs NPRPC

Feature-rich multi-transport RPC (TCP/WS/HTTP3/QUIC/SharedMemory) for C++/TS/Swift with FlatBuffers. Strong where Callwire doesn't go (C++, browsers, QUIC), but requires .npidl schemas and code generation. No orchestration layer.


Moat

Callwire's defensible advantages:

  1. Zero-schema across 4 runtimes — no other library lets you export a function in Go/Python/Rust/TS and call it from any of the others without a schema definition or codegen step.

  2. Built-in orchestrationcallwire init auto-detects workers across all languages from a single config file. Competitors require external process managers (supervisord), container orchestration (Kubernetes), or bespoke shell scripts.

  3. Bidirectional symmetry — the same connection serves both client and server roles. Only protosocket offers this at the transport level; gRPC, Thrift, and Cap'n Proto enforce client/server roles at the API level.

  4. Protocol simplicity — 4-byte length prefix + MessagePack. The entire spec fits on one page (SPEC.md). Implementing from scratch takes hours, not weeks. This is the opposite of HTTP/2 (gRPC), which requires thousands of lines of HPACK, flow control, and stream multiplexing.

  5. Per-language CLI — each SDK ships its own callwire init so there's zero cross-language dependency at build or runtime.

  6. Polyglot performance — MessagePack encoding is fast in every runtime. Callwire doesn't optimize for a single language at the expense of others; the framing layer is simple enough that every language gets near-native serialization.