One node is enough for many applications and needs no other service. Run several nodes when you need more capacity or want to restart without disconnecting everyone at once. There are two ways to let nodes share events, presence and channel occupancy:
| Redis (this page, first half) | Erlang cluster (second half) | |
|---|---|---|
| Status | Stable. Recommended. | Experimental. |
| Needs | A Redis server | Nodes that can reach each other over Erlang distribution |
| Best for | Any backend language, Docker, most setups | Elixir teams who already cluster their nodes, or who want no Redis |
| Scale | Tested with the Redis failure cases below | Suits tens of thousands of connections; see its limits below |
Use one or the other, never both: setting a Redis URL and cluster: :erlang
together is rejected at startup.
With Redis
Nodes coordinate through a single Redis.
Setup
On every node set the same:
SOCKETELX_APP_ID,SOCKETELX_APP_KEY,SOCKETELX_APP_SECRETSOCKETELX_REDIS_URL, for exampleredis://:password@redis:6379(rediss://for TLS)
Put a load balancer in front. deploy/nginx.conf is an example and
docker-compose.example.yml runs two nodes, Redis and nginx together:
docker compose -f docker-compose.example.yml up --build
No sticky sessions are needed. A WebSocket stays on the node it connected to, and REST calls from your backend can land on any node.
Embedded: {Socketelx, name: MyApp.Socketelx, redis_url: "redis://localhost:6379", ...}.
What is shared
- An event sent to any node reaches subscribers on every node.
- Presence is shared.
member_added/member_removedfire once per user for the whole cluster. channel_occupied/channel_vacatedwebhooks fire once cluster-wide.- The channel info endpoint counts subscribers on all nodes, and the channel
list (
GET /apps/:app_id/channels) includes channels from every node. - Rate limits,
SOCKETELX_MAX_CONNECTIONSand metrics are per node, not shared.
When things fail
| What happens | Result |
|---|---|
| Redis becomes unreachable | Every node keeps serving its own clients. Delivery between nodes pauses and resumes when Redis returns. Nothing crashes. |
| Redis restarts empty or is flushed | Running nodes write their subscriptions and presence back within about 10 seconds. Clients are not re-announced as new members. |
| A node crashes or is killed | Its heartbeat key expires (30 s); a surviving node then removes its presence and occupancy and announces member_removed / channel_vacated. Expect ghost members for roughly 30 to 40 seconds after a hard crash. |
| A node is stopped cleanly | Clients are told to reconnect, then connect to another node. |
Things to know
- Redis holds only live, ephemeral state. It needs no persistence and stays small.
- Delivery is at-most-once. A client that is disconnected when an event is sent does not receive it later.
- Joining or leaving a presence channel scans that channel's members inside Redis. That is fine for normal presence channels (Pusher itself caps them at 100 members) but not for huge ones.
- Redis Cluster (sharded) is not supported. Use a single endpoint: plain, Sentinel, or a managed primary/replica service.
- Keys are prefixed
socketelx:<app_id>, so several apps can share one Redis.
Checking it works
Connect a client to node 1, send an event through node 2's REST endpoint, and confirm the client receives it. Then stop a node and restart Redis and confirm clients and presence recover.
Without Redis: Erlang cluster (experimental)
Experimental. It passes automated tests across real separate nodes, but it has seen far less real-world use than the Redis mode. Try it on staging, read the limits below, and prefer Redis for anything critical. Its behaviour and settings may still change before 1.0.
Elixir and Erlang nodes can talk to each other directly, so Socketelx can share state with no extra service. Turn it on and tell the nodes about each other:
SOCKETELX_CLUSTER=erlang
or, embedded, {Socketelx, name: MyApp.Socketelx, cluster: :erlang, ...}.
Every node needs the same app id, key and secret, as with Redis.
Standalone (Docker)
The image is an Elixir release, so the Erlang node settings are the usual release variables. On every node:
| Variable | What to set |
|---|---|
SOCKETELX_CLUSTER | erlang |
RELEASE_COOKIE | The same long random secret on every node (for example openssl rand -hex 32). It is the only thing that authenticates nodes to each other. |
RELEASE_NODE / RELEASE_DISTRIBUTION | The node's own name. The default (socketelx@<hostname>, short names) works when each node has a resolvable hostname, as in the Compose example. Use RELEASE_DISTRIBUTION=name with RELEASE_NODE=socketelx@<ip-or-fqdn> when nodes find each other by IP. |
SOCKETELX_CLUSTER_NODES | Comma-separated names of the other nodes, for example socketelx@socketelx-1,socketelx@socketelx-2. |
SOCKETELX_CLUSTER_DNS | Alternative to the list: a hostname that resolves to every node's IP (a Kubernetes headless service, a Docker Swarm tasks.<service> name). Nodes are then expected to be named socketelx@<ip>, so set RELEASE_DISTRIBUTION=name and RELEASE_NODE=socketelx@$POD_IP. Change the part before the @ with SOCKETELX_CLUSTER_NODE_BASENAME. |
A ready-to-run two-node example without Redis is in
docker-compose.cluster.example.yml:
CLUSTER_COOKIE=$(openssl rand -hex 32) docker compose -f docker-compose.cluster.example.yml up --build
Nodes keep retrying every few seconds, so start order does not matter.
Embedded
If your application already forms a cluster (for example a Phoenix app with
dns_cluster or libcluster), add cluster: :erlang to the Socketelx child
spec and you are done. Otherwise pass cluster_nodes: [:"app@host1", ...] or
cluster_dns: "my-app.internal". The application must be started as a named
node (--sname / --name, or as a release).
What is shared
The same as with Redis: events reach every node, presence and the
member_added / member_removed / channel_occupied / channel_vacated
webhooks fire once for the whole cluster, and the channel list and counts cover
all nodes. Rate limits, the connection cap and metrics stay per node.
How it works
- Events are delivered to the sender's own sockets, then sent straight to the other nodes. There is no central point in the path.
- Presence and occupancy are decided by one leader node (the same one every node picks, with no voting) so a user's first and last connection are recognised exactly once. Every other node keeps a standby copy.
When things fail
| What happens | Result |
|---|---|
| A node is stopped cleanly | Clients are told to reconnect; the leader announces what left. |
| A node is killed or loses its network | The leader drops what that node owned and announces the departures once, as soon as Erlang notices the node is gone. |
| The leader's node dies | The next node takes over from its standby copy, asks every node what it owns, and announces only the differences. Presence changes wait briefly (about two seconds) for the new leader; events between surviving nodes keep flowing. |
| The network splits in two | Each side keeps working on its own with its own leader. When it heals, the sides are merged: clients and webhooks are told what each side missed. |
Limits
- Throughput of presence. The leader handles every subscribe, unsubscribe and presence change in the cluster. That suits tens of thousands of connections, not millions. Use Redis if you need the most headroom.
- Failure detection time. A node that vanishes without closing its
connection is noticed after Erlang's
net_ticktime(60 s by default). Lower it withERL_AFLAGS="-kernel net_ticktime 15"for faster cleanup. - Security. Erlang distribution trusts anyone with the cookie, and a node
with the cookie can run any code. Never publish port 4369 or the distribution
ports (restrict them with
ERL_AFLAGS="-kernel inet_dist_listen_min 9100 inet_dist_listen_max 9105"), keep them on a private network, and setRELEASE_COOKIEyourself. The cookie baked into a public image is not a secret. - Rolling deploys. Nodes running different Socketelx versions may not agree on the cluster protocol. Upgrade all nodes together, or accept a short period of inconsistent presence.
- Delivery is at-most-once, like Pusher itself.
Checking it works
Start two nodes, connect a client to one and trigger an event through the
other. To see the cluster itself, open a shell on a node
(docker compose exec socketelx-1 bin/socketelx remote) and run Node.list();
it should list the other nodes.