Run a Two-Node Mesh Cluster in Minutes¶
Connect two FastAPI nodes and synchronize shared state with Mesh in under five minutes. Install, create nodes, join a cluster, and read shared data.
This guide walks you through installing Mesh, wiring it into two FastAPI applications, joining them into a peer-to-peer cluster, and verifying that state syncs correctly across both nodes. By the end you will have a working two-node Mesh cluster running on your local machine.
Install Mesh¶
Mesh is now available in in PyPI.
The recommended method install meshd is to use uv
Alternatively, you can also add it directly to your project through uv
You can also install it directly from the GitHub repository:
Alternatively, clone the repository and install in editable mode - useful if you want to inspect or modify the source alongside your own application code:
Create your first node¶
Create a file called node_a.py. Import FastAPI and Node, instantiate both, and expose a simple endpoint that returns the node’s current shared state:
from fastapi import FastAPI, Body
from meshd.node import Node
app = FastAPI()
node = Node(name="node-a", app=app, action_on_conflict="merge")
@app.get("/")
async def root():
return node.data.to_dict()
@app.post("/push-data")
async def push(payload: dict = Body(...)):
await node.put_data(payload)
return node.data.to_dict()
Passing your app instance to Node is all it takes to register the /meshd WebSocket endpoint. Mesh handles the rest automatically.
Start a second node and join the cluster¶
Create a file called node_b.py. Use a lifespan context manager to join node-a during startup, sync state, and deregister cleanly on shutdown:
import asyncio
import contextlib
from fastapi import FastAPI
from meshd.node import Node
SLEEP = 1
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: wait briefly, then join node-a and sync
await asyncio.sleep(SLEEP)
await node.join(["ws://localhost:8000/meshd"])
await asyncio.sleep(SLEEP)
await node.sync_up()
await asyncio.sleep(SLEEP)
# Register this node in the shared state
await node.put_data({"__node_b__": {"name": "node-b", "status": "up"}})
yield
# Shutdown: remove self from shared state and propagate
await node.pop_data("__node_b__")
await node.sync_up()
await asyncio.sleep(SLEEP)
app = FastAPI(lifespan=lifespan)
node = Node(name="node-b", app=app, action_on_conflict="merge")
@app.get("/")
async def root():
return node.data.to_dict()
Start node-a on port 8000 first, then start node-b on port 8001. The lifespan hook fires automatically when uvicorn brings the application up.
Verify sync¶
After sync_up() completes, both nodes hold an identical snapshot of the shared MonotonicDict. Send a GET / request to each node and confirm the responses match:
curl http://localhost:8000/
# {"__node_b__": {"name": "node-b", "status": "up"}}
curl http://localhost:8001/
# {"__node_b__": {"name": "node-b", "status": "up"}}
Any write made through put_data() on either node propagates to the other automatically. You do not need to poll or manually trigger additional syncs. Mesh pushes state changes to all connected peers as they happen.