sleipnir
Hello everyone,
We’d like to share Mesh, a new open-source Elixir library developed by the Eigr community.
Mesh was created as part of the work on Spawn, where we needed a reliable and scalable way to route messages to specific groups of processes across a cluster, with deterministic behavior and minimal coordination overhead.
During Spawn’s development, we evaluated existing solutions such as :pg, Horde, and ProcessHub. These are solid libraries and work very well for many use cases. However, for our specific requirements — especially around capability-based routing, deterministic ownership, and very large-scale clustering — none of them fit exactly what we needed. Rather than forcing a model that didn’t align with our constraints, we decided to design a small, focused abstraction that could serve as a foundation for Spawn and similar systems.
That abstraction became Mesh.
What Mesh is
Mesh is a library for managing virtual processes and routing messages to them based on capabilities, rather than direct PIDs or global names.
At a high level, Mesh provides:
-
Capability-based process registration
-
Deterministic routing using shards
-
Decoupling between callers and process location
-
A model where any node can invoke a virtual process, and the system resolves where it should run
Processes register themselves with one or more capabilities, and callers route messages by capability and key. Mesh handles shard ownership and node selection, allowing systems to scale without relying on global process registries.
Example usage
Registering a process with a capability:
Mesh.register_capabilities([:game, :chat])
Routing a message to a virtual process by capability and key:
{:ok, pid, response} = Mesh.call(%Mesh.Request{
module: MyApp.GameActor,
id: "player_123",
payload: %{action: "move"},
capability: :game
})
Mesh also exposes simple functions for understanding cluster state:
Mesh.nodes_for(:game) #=> [:node1@host, :node2@host]
Mesh.all_capabilities() #=> [:game, :chat, :payment]
These helpers return lists of nodes that support a given capability or all capabilities registered in the cluster
Under the hood, Mesh computes a shard from the routing key, determines the owner node for that shard and capability, and delivers the message to the appropriate process — locally or remotely — without the caller needing to know where that process lives.
This makes Mesh particularly useful for systems that need:
-
Logical actors
-
Deterministic placement
-
Clear separation between routing logic and business logic
Project status
Mesh is new and under active development. The API is intentionally small, and we expect it to evolve as we continue integrating it into Spawn and gather feedback from real-world usage.
We’re sharing it early because we believe the underlying ideas may be useful beyond our own projects, especially for people building distributed systems on the BEAM who need more control over routing semantics.
Links
Documentation:
https://hexdocs.pm/mesh/Mesh.html
Hex package:
Spawn:
GitHub repository:
Feedback, questions, and contributions are very welcome.
— Eigr community
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 27 to 18- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
garrison
tbh y’all need to chill this thread is kinda unhinged
It seems to me like you know what you’re doing and wrote a library that makes sense for your case, but when describing it in the readme you (understandably) tried to put it into very general terms. But because it’s very general everyone is just going to project their own experiences onto it instead.
In a similar situation I found that explicit examples help. The sidecar thing makes sense to me; consider adding it to the readme.
sleipnir
I think it’s normal to be unfamiliar with all possible terms in the world of technology. I didn’t mean background job, and it certainly has nothing to do with LDAP.
A sidecar is a container that runs alongside another container within a POD. This sidecar container can provide any kind of extra functionality that the main container depends on and that, for some reason, it doesn’t execute on its own.
Here’s a random examples from the internet of what a sidecar is:
Here’s something closer to what I described as a use case: https://www.youtube.com/watch?v=b86DIo0_UoU
And here’s the project that will use Mesh after we stabilize the Mesh API:
https://github.com/eigr/spawn
derek-zhou
Sorry, I am not familiar with the term “sidecar”. Do you mean a background job? A server-less function? A system optimize to run hour long simulation jobs will need very different architecture from a system to execute serverless functions finishing in a few milliseconds.
What you described feels like a directory service, which may not even need to be distributed in nature. LDAP has been there for decades; when properly configured it is pretty fast too.
sleipnir
In a sidecar-based system, clients in other programming languages register with a hostname. These containers have routines that contain entities, and these entities have names. They share the same cluster, and the sidecar executes the calls between the various containers via erlang dist. For this to work, the sidecar in Elixir cannot choose a random Node in the cluster because there is an affinity between the sidecar and the host application, and therefore it would fail to call a sidecar that does not have an affinity with the destination sidecar. So we need:
1 - To know which hosts have which entities
2 - When a client invokes an entity, the cluster must call exactly the sidecar that has a host that has the destination entity.
That’s exactly what the library was created for, and it meets these requirements. This happens in practice. However, I see that it’s possible to explore the library for other use cases. For example, I could have applications separated by region and could route a call based on the client’s region. Or I could have specific workloads for specific tasks and route based on that. There are many possible use cases.
derek-zhou
Of course I can imagine all that, I even gave you 3 examples so you can tell me which one is closer to your application, and you did not answer my question. My point is: the need for affinity could be mandatory, or advisory, or anything in between. I do not think your library can cover all bases, at least not now, right?
When I “shop” for a library, I look for 3 things, in this order:
You have given 3, but not 1 and 2.
sleipnir
Imagine you have a cluster of shared applications that aren’t homogeneous—that is, they are different applications, such as games, chat applications, and others—but for whatever reason you want them to be part of an Erlang cluster. You want to logically group these applications within the same cluster and be able to invoke processes using any label (as mentioned before, not a PID). You expect a process to be started and maybe monitored when this request occurs, and that eventually, this request will go to the same node as long as there is a live process on that node. That’s basically it. We don’t want strong consistency, replication of user process state, nothing like that; it’s eventual consistency and it’s just service discovery. You can implement your own selection strategy if you want.
sleipnir
Oh man, that was rather sad.
I appreciate all constructive feedback, and as I said in the post, the idea was to share it very early to receive positive feedback, without any pretense of being perfect or academic.
I think you made a number of assumptions, both about the library itself and about me, whom you don’t know. I have 25 years of experience, many of which have been spent working with distributed systems, and I have read many books and implemented many systems in production. I also contribute to many libraries, many of them about distributed systems, not only from the Elixir ecosystem but from other ecosystems as well, and I always try to be polite to everyone I interact with.
You mentioned Horde; I am one of the contributors/maintainers of the project, and I am sure I know it very well. Anyway, thank you for the feedback; I appreciate it very much and will take it into account whenever possible.
derek-zhou
Can you articulate your intended usage scenario with a concrete example? Do you want “node affinity” based on performance concerns, ie. hot cache, or physical attributes of the nodes ie, big memory vs small memory, or security/regulation concerns ie. to make private enclaves within a public cloud?
Asd
I’ve just reread my post, and it sounds a bit toxic. I want to make clear that given that this implementation is poor, it is a good start and the whole idea is not strange, but it is a classic problem which was solved several times by other authors from Elixir community and what you’re doing with
Meshis a good starting point to explore the problem spaceMy personal recommendation would be to
Hordelibrary solves the similar problem, but it uses eventually consistent delta-CRDT merkle tree structure to (eventually) maintain the same version of the information about which processes belong to what groups. Other good example isProcessHubwhich is, afaik, still in active development phase.I am looking forward to seeing the new version of Mesh which would use some interesting distributed algorithm. Happy coding
Asd
I’ve read the code and here’s my review
TLDR
This is a poor library with a lot of bugs, misleading documentation and poor applicability.
What does it actually do?
It essentially does 2 things.
GenServer.callwith specified payload Each node in the group is going to maintain ashard_countof processes per each capability called “actors” in terms of this library (but I call them just shards here) and this request is routed to one of these shards. That means if the node has two capabilities and max shard count is 10, it is going to maintain 20 processes (all of them are lazily initialized).Problems
Setting capabilities
Capabilities is a distributed data, and as all such data it is subject to CAP. Current solution is not qualifying for any of those letters and, putting CAP aside, provides very poor guarantees. If other node can’t start the
Meshand set the capabilities in time, the current node won’t know about it. It uses replication onnet_kernel.monitor_nodesevents, which just tries to do asynchorous rpc to get the capabilities of other node and share current capabilities with it.That means that any change to cluster configuration or groups is eventually consistent (at best) and it is quite possible that your request would get routed to the already dead node. If so, this library provides no fallback and you’d just have a dead request
Picking a node
Alright, let’s forget about previous paragraph and imagine that every node has the exactly the same information about the groups (aka “capabilities”).
In pseudo-code it looks like this
And it has many problems. First thing is that routing to the node is not consistent. Second is that shard within node is selected twice, which is a bug.
Then, and it is the most beautiful bug here,
shard_idis used as a key for selecting a node in a group and the shard process. That means, that if I have two nodes and 20 shards on each node, only 10 shards on each would receive the request, which is very fun. In general, if I have N nodes, onlyshards / Namount of shard will receive the request.Because if I hit the first node on the list of two nodes, that maens that
shard_idis dividable by two, and all shards withshard_idnot dividable by two won’t receive a request on this node ever.And for problems which I couldn’t reflect in the pseudo code:
GenServer.start_link(Module1, ...), and if I then send a request which would end up in the same shard, but with Module2 passed as the implementation, it would still hit the shard created by the first request, with Module1Other problems
ActorTableprocess on each node which just does nothing.PartitionSupervisorMesh.Cluster.Membershipschedules monitoring after 100ms, while it can just do it in handle_continueglobalfor locking on only the current nodeConclusion
Unclear use-case, bad implementation, misleading documentation.
Same functionality (but without a lot of bugs) can be achieved by using any other process group solution (including bultin
global_group,pgand third-party Horde, Swarm, ProcessHub?, syn).But given that shard processes are expected to be stateless and rounting to be inconsistent, it all boils down to just two lines