Quickstart
Build a node, put something on it, and prove the node accepted it. That is the whole of this page. It is deliberately smaller than a first program usually is, because the first thing worth knowing about Net is which of two node types you are holding.
Two node types, and picking the wrong one costs an afternoon
Every binding gives you both. They are not layers of each other and the names are close enough to be worth stating plainly:
- A bus node is in-process. It ingests events into a local ring buffer, counts them, and hands them back when you poll or subscribe. No network, no peers, no keys. This is what you build to check your install and what most tests use.
- A mesh node speaks encrypted UDP to other nodes. It carries capabilities, tools and nRPC, and it is the node the rest of this spine (announce onward) is about.
If you are here to move events inside one process, you want a bus node and you can stop after the first snippet. If you are here to have two programs find and call each other, you want a mesh node, and you need the next two sections.
A mesh node needs a shared key and a handshake
Two facts that are easy to skip and expensive to skip:
Every mesh node in a mesh uses the same pre-shared key. It is 32 bytes. The bindings disagree about whether you hand them raw bytes or a 64-character hex string, and that disagreement is real rather than cosmetic — the fragment below says which one yours takes.
Peers do not find each other by magic. A mesh node has to be connected to
another mesh node before anything folds between them: one side accepts, the other
connects, and both start. Until that happens, announce announces to nobody and
discover returns an empty list — which looks exactly like a bug in your
capability filter and is not one.
This is the single most common way a first mesh program fails: it is correct, and it is talking to itself.
Acceptance is not delivery
Whatever you put on a node, the call that accepts it is telling you one thing: the local node took it. It is not telling you a subscriber ran, a peer received it, or the work is done. That distinction has a page of its own — Submitted Is Not Completed — and it is the model, not an implementation detail to be optimized away later.
The first program therefore verifies a local counter rather than claiming a round trip. Every fragment below ends by asserting the node's own ingested count, because that is the strongest claim the smallest program can actually make.
Why your first program appears to hang
The default transport is memory, and memory discards events after counting them. A first program that publishes and then waits to read the same event back will not error. It will sit there, waiting for something that is never coming.
Reading events back needs either an adapter that retains them (Redis, JetStream) or the mesh transport between two nodes. That is a deliberate second decision, not a default you were supposed to have found.
Where this goes next
The seven pages of this spine are one journey, in this order: build a node → announce a capability → discover it from somewhere else → invoke it → watch what it emits → move artifacts too large for the bus → and handle the ways it fails.
Those links are language-neutral: each one states the objective and hands you the four lenses. Once you are inside a language the Next control keeps you there — it will not walk a Python reader into Go.
Build it — Python
pip install net-mesh-sdk # the ergonomic SDK — imports as net_sdk
pip install net-mesh # the native binding — imports as netInstall both. Two packages, two import names, and none of the four strings match:
net-mesh-sdk → net_sdk, net-mesh → net. A ModuleNotFoundError here almost
always means the right package is installed under the other name. Python 3.10 or
newer.
Parts of this spine are reachable only on net, never on net_sdk. That is not a
gap to work around — it is where the surface lives.
A bus node
from net_sdk import NetNode
with NetNode(shards=4) as node:
node.emit({"sensor": "lidar", "range_m": 12.5})
node.emit_raw('{"sensor": "radar", "range_m": 45.0}')
node.emit_batch([{"a": 1}, {"a": 2}, {"a": 3}])The context manager does the shutdown and drain. Leaving it out is how a Python program exits with a drain worker still holding events.
Transports are constructor arguments: NetNode(shards=4) is memory,
NetNode(shards=4, redis_url="redis://localhost:6379") and jetstream_url=… use
those backends behind the same emit / subscribe code.
A mesh node
from net_sdk import MeshNode
node = MeshNode(bind_addr="127.0.0.1:9000", psk="42" * 32)Python takes the PSK as a 64-character hex string, not raw bytes. "42" * 32
is the hex form of the same 32 0x42 bytes Rust and TypeScript pass as an array —
if you are pairing a Python node with one of those, this is the line where the two
representations have to agree.
MeshNode is a plain object with no context manager. Call shutdown() yourself.
The handshake
Both halves have to be in flight at once. accept and connect each block
until the handshake completes, so the responder goes on its own thread:
import threading
HOST_ADDR = "127.0.0.1:9001"
host = MeshNode(bind_addr=HOST_ADDR, psk="42" * 32)
agent = MeshNode(bind_addr="127.0.0.1:9000", psk="42" * 32)
responder = threading.Thread(target=host.accept, args=(agent.node_id,))
responder.start()
agent.connect(HOST_ADDR, host.public_key, host.node_id)
responder.join()
host.start()
agent.start()Calling them in sequence on one thread cannot work. host.accept(...) waits
for an initiator that the next line was going to be, so it never returns, and
the failure arrives ~20 seconds later looking like a network problem:
RuntimeError: accept: connection error: handshake timeoutA plain thread is enough; the call releases the GIL while it waits. This is the
one place Python differs from Rust on this page, where tokio::join! does the
same job.
node_id and public_key are properties, not methods — no parentheses. It is
a small thing that produces a confusing error, because host.public_key without
the call still evaluates to something and gets passed along.
Verify it worked
from net_sdk import NetNode
with NetNode(shards=1) as node:
node.emit({"msg": "hello, mesh"})
stats = node.stats()
assert stats.events_ingested == 1, "the bus did not accept the event"
print(f"accepted: ingested={stats.events_ingested}")Expect one line, accepted: ingested=1. The counter is read at the producer
boundary: accepted by this node, not received by anyone.
Next: Announce a capability.