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.tomlspawns 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 )
// Call a remote function
client, _ := callwire.Connect("localhost:9090")
result, _ := callwire.Ref[int](client, "add")(10, 20) // 30
Python
# 1. Export a local function (makes it server-ready)
return +
# 2. Dynamic module import (connects & invokes dynamically)
= # 30
Rust
use ;
register_unary;
let client = connect.await?;
let result: i64 = client.import.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
Orchestration (v2)
Workers are auto-discovered by the callwire init CLI and declared in callwire.toml:
[]
= "my-project"
= "1.0.0"
[]
= "cd go/callwire && go run examples/server.go"
= "./bin/go-worker"
[]
= "cd rust && cargo run --quiet --example my-worker"
= "./bin/rust-worker"
Generate it with any of the four native CLIs — they all produce the same output:
# Python
PYTHONPATH=python
# Go
&&
# Rust
# TypeScript
Then call init() — Callwire starts a registry, spawns workers, and routes everything automatically:
# reads callwire.toml, spawns workers
# Import functions dynamically as if they were local!
= # → routed to Go worker
= # → routed to Rust worker
See the full demo → examples/2_orchestrated/demo.py
FastAPI integration
await
yield
await
=
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
= # routed transparently via registry
// Rust — connect to registry, route calls transparently
let client = connect_registry.await?;
let sum: i32 = client.import.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)
// Go — TLS client (with optional mTLS)
client, _ := callwire.ConnectWithReconnectTLS("localhost:9090", callwire.TLSConfig)
# Python — TLS client
// Rust — TLS client
let client = TlsConfig
.connect.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
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
&&
# Python
&&
# Rust
&&
# TypeScript
&&
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:
-
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.
-
Built-in orchestration —
callwire initauto-detects workers across all languages from a single config file. Competitors require external process managers (supervisord), container orchestration (Kubernetes), or bespoke shell scripts. -
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.
-
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.
-
Per-language CLI — each SDK ships its own
callwire initso there's zero cross-language dependency at build or runtime. -
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.