Scaling WebSockets the Wrong Way (On Purpose): A Multi-Instance Reality Check

You don’t truly understand distributed systems until your app “works perfectly”… and then breaks the moment you scale it.
🚧 The Illusion of “It Works on My Machine”
In Phase 1, we built a clean WebSocket chat server using Go.
Single instance. In-memory hub. Messages flow beautifully.
Everything works.
So naturally, the next step is:
“Let’s scale it.”
And this is where things start to get… interesting.
🧠 The Core Idea
Phase 2 intentionally introduces a broken architecture:
Two independent WebSocket servers
Each with its own in-memory hub
No shared state
nginx load balancing connections
At first glance, this looks like horizontal scaling.
In reality, it’s a trap.
In-memory state does not magically become distributed just because you added more containers.
🏗️ Architecture Overview
nginx (round-robin)
├── server-1 (hub A)
└── server-2 (hub B)Clients connect through nginx, but:
Alice and Charlie land on server-1
Bob lands on server-2
All three join the same room.
Sounds fine… until messages start flying.
⚙️ What Changed in This Phase?
Minimal code changes. Maximum impact.
1. Instance Awareness
Each server now identifies itself:
serverID := os.Getenv("SERVER_ID")
log.SetPrefix("[" + serverID + "] ")This is surprisingly powerful—every log line now tells you which instance handled it.
2. /instance Endpoint
{"server_id":"server-1"}This lets clients confirm where they’re connected.
Because debugging distributed systems without visibility is just guessing with extra steps.
3. Dockerized Multi-Instance Setup
server1 → :8081server2 → :8082
nginx →:8080(round-robin)
No shared memory. No Redis. No tricks.
Just raw isolation.
🧪 The Demo That Breaks Your Assumptions
Three clients:
Client | Instance | Expected Behavior |
|---|---|---|
Alice | server-1 | Sends message |
Charlie | server-1 | Receives ✅ |
Bob | server-2 | Receives ❌ |
What Happens:
Alice sends a message to
room: generalCharlie receives it instantly
Bob… gets nothing
Timeout.
Silence.
Existential crisis.
📉 The Result
Charlie received: ✅
Bob received: ❌ NONE (timeout after 2s — expected)And just like that:
Your “scalable” system is not actually scalable.
💥 Why This Happens
Each server has:
Its own memory
Its own connection list
Its own “truth”
There is no communication between instances.
So when Alice sends a message:
server-1 broadcasts to its clients ✔
server-2 has no idea anything happened ❌
This is the exact moment most developers realize:
Stateless scaling works for HTTP… but not for real-time systems.
🧩 What This Teaches You
This phase isn’t about fixing anything.
It’s about feeling the failure.
Because once you see it, you can’t unsee it.
Key takeaways:
Horizontal scaling ≠ shared state
WebSockets are inherently stateful
Load balancers don’t sync your data
In-memory hubs don’t scale beyond one instance
🧠 The Real Problem
You don’t have a WebSocket problem.
You have a distributed systems problem.
And those don’t get solved with:
More containers
More CPU
More hope
They get solved with shared communication layers.
🚀 What Comes Next (Phase 3)
Now that we’ve broken the system on purpose, we fix it properly:
👉 Introduce Redis Pub/Sub
Each instance publishes messages
All instances subscribe
Messages propagate across servers
Suddenly:
Alice → server-1
Bob → server-2
Charlie → server-1
…and everyone gets the message.
Like magic. Except it’s just architecture.
🧾 Final Thoughts
If your WebSocket app works perfectly in a single instance, congratulations—you’ve solved the easy part.
The real test is this:
What happens when your second server shows up?
If the answer is “everything still works,”
you’ve done it right.
If not—welcome to distributed systems.
Stay in the loop
Get notified when new posts are published. No spam, unsubscribe anytime.
No spam · Unsubscribe anytime