Clustering Fundamentals: Scaling Through Unity — Deep Dive
How to use this page. This is the long-form reference for Clustering Fundamentals. The short page gives you the 60-second summary (why clusters exist, the three flavours, a Node.js
clustersnippet). This page gives you the full mental model: the algorithms that hold a cluster together, the failure modes, and the code you would actually ship in production — single-process, multi-process on one box, and multi-node with Docker / HAProxy / Kubernetes.
1. The Mental Model — What a Cluster Actually Is
A cluster is a group of independent servers (or processes) that cooperate to look like one logical system to the outside world. The outside world talks to a single virtual endpoint; the cluster decides who answers.
Two ideas hide inside that picture:
- Membership — "Who is currently in this cluster, and who is the leader?"
- Coordination — "How do they share state, agree on a leader, and survive when one dies?"
Everything below this line is a technique for solving one of those two problems.
2. Why Cluster? The Four Forces
| Force | Single server | Cluster |
|---|---|---|
| Scalability | Vertical only — buy a bigger box | Horizontal — add another box |
| Availability | One crash = full outage | One crash = the rest take over |
| Latency | Single region only | Place nodes near users |
| Cost | Big iron, fixed capacity | Commodity boxes, pay-as-you-grow |
The trade-off: clusters buy you all four, at the price of complexity (consensus, replication, partitions, split-brain). The whole rest of this page is "how to manage that complexity."
3. The Three Flavours of Cluster
3.1 Load-balancing clusters (performance)
Distribute incoming requests across stateless workers. Each node is interchangeable; the load balancer picks one per request.
Best for: stateless web/API servers, CDN edges, microservices.
3.2 High-availability clusters (reliability)
One node is active, the other is passive (or several stand by). They share state via replication; if the active dies, a passive is promoted.
Best for: databases (PostgreSQL streaming replication, MySQL Group Replication), message brokers (Kafka brokers, RabbitMQ mirrored queues), critical control planes.
3.3 Compute / HPC clusters (raw power)
Slice one huge problem into many tiny pieces and run them in parallel. The work is the unit of parallelism, not the request.
Best for: AI/ML training (PyTorch DDP, Horovod, Ray), scientific simulation, batch ETL, MapReduce.
💡 Most production systems are a hybrid: an LB cluster of stateless web nodes in front of an HA cluster of stateful databases, all orchestrated by a compute-style scheduler like Kubernetes.
4. Cluster Coordination — The Four Hard Problems
No matter the flavour, every cluster has to solve the same four problems.
4.1 Membership — "Who is alive?"
Nodes ping each other on a timer. Missing two heartbeats in a row → declare a node suspect, then dead.
4.2 Leader election — "Who is in charge?"
Many distributed algorithms exist (Raft, Paxos, Zab). The simplest mental model is lease-based election: a node wins a lock in a shared store (etcd, ZooKeeper, Consul), renews it before it expires, and steps down if it can't.
📖 See Leader Election in the Design Patterns reference for the full breakdown.
4.3 State replication — "How is data kept in sync?"
Three common strategies:
| Strategy | How it works | Trade-off |
|---|---|---|
| Synchronous | Write waits for all replicas to ack | Strongest consistency, highest latency |
| Asynchronous | Write returns after primary acks; replicas catch up later | Fast writes, risk of data loss on primary crash |
| Quorum (N of M) | Write needs ⌊N/2⌋+1 acks | Balanced — tunable consistency vs latency |
This is also where CAP enters: during a network partition you must choose between Consistency (reject writes) and Availability (accept writes, reconcile later).
4.4 Failure detection — "Is this node really dead, or just slow?"
Two kinds of failure:
- Crash failure — process is gone. Easy to detect (TCP RST, no heartbeat).
- Slow / GC pause — process is alive but unresponsive. Hard to distinguish from network delay. Tools: Phi accrual failure detector (Akka), SWIM gossip protocol (HashiCorp Serf / Consul).
5. Architecture Patterns for Clustered Systems
5.1 Stateless worker pool
The workhorse. Workers hold no local state; everything lives in a shared DB and cache.
Why stateless wins: any worker can take any request, so you can lose one and add one with zero ceremony. The price is that every request becomes a network round-trip to cache/DB — see § 5.3 for the cure.
5.2 Active-passive (HA) with virtual IP
The classic HA pattern. Two nodes share a Virtual IP (VIP); only one owns it at a time. If the active dies, the standby grabs the VIP with arping/gratuitous ARP so the network re-routes traffic.
5.3 Sticky sessions / session affinity
Pure stateless hurts when sessions are expensive (auth, shopping cart, websocket). Pin a client to one node for the session's lifetime — usually via a cookie hashed onto the worker pool.
⚠️ Watch out: if Node A dies, all its sessions are lost unless you store them externally (Redis/Memcached). The Amazon-meets-Google pattern is "sticky-by-default, but the source of truth lives in Redis."
5.4 Sharded cluster
When one node can no longer hold all the data, you partition (shard) it. Each node owns a slice; a router decides which slice based on the key.
This is the foundation of Cassandra, MongoDB, Elasticsearch, Vitess, and CockroachDB.
📖 Full deep dive in Design Patterns → Sharding.
6. Cluster Topologies at Three Scales
6.1 Single-machine: Node.js cluster
Use all CPU cores on one box. Master forks a worker per core; workers share the listening socket via SO_REUSEPORT.
// multi-core on one machine — the smallest possible "cluster"
const cluster = require("node:cluster");
const http = require("node:http");
const os = require("node:os");
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary ${process.pid} forking ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on("exit", (worker, code, signal) => {
console.log(
`Worker ${worker.process.pid} died (${signal || code}). Replacing...`
);
cluster.fork(); // self-healing on a single machine
});
} else {
http
.createServer((req, res) => {
// Heavy CPU work is now parallel across cores
res.writeHead(200);
res.end(`Hello from worker ${process.pid}\n`);
})
.listen(8000);
}Trade-offs:
- ✅ Free; zero infra.
- ✅ Self-healing on a single host.
- ❌ Still a single host. If the host dies, the whole "cluster" dies.
- ❌ State is per worker — use Redis or sticky session if you need shared state.
6.2 Multi-machine: Docker Compose + HAProxy
Two or more VMs/containers, a TCP load balancer on top, and shared state in Redis.
docker-compose.yml:
services:
haproxy:
image: haproxy:2.8
ports: ["80:80"]
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on: [app-a, app-b]
app-a:
build: .
environment:
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgres://postgres@db:5432/app
app-b:
build: .
environment:
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgres://postgres@db:5432/app
redis:
image: redis:7-alpine
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: examplehaproxy.cfg — Layer-4 round robin with a health check:
frontend fe
bind *:80
default_backend be
backend be
balance roundrobin
option httpchk GET /healthz
server app-a app-a:3000 check inter 2s fall 3 rise 2
server app-b app-b:3000 check inter 2s fall 3 rise 2Now adding a third node is one line in the compose file — no application change.
6.3 Multi-region: Kubernetes + Ingress
Kubernetes takes the Docker Compose idea and adds declarative desired state: you say "I want 5 replicas," the controller scheduler makes it true.
# cluster-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 5 # ← "I want 5 of these"
selector: { matchLabels: { app: api } }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: registry.example.com/api:1.4.2
ports: [{ containerPort: 3000 }]
readinessProbe: # ← "remove from LB if unhealthy"
httpGet: { path: /healthz, port: 3000 }
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata: { name: api }
spec:
selector: { app: api }
ports: [{ port: 80, targetPort: 3000 }]The Service object is a virtual IP + load balancer that kube-proxy programs into every node's iptables/IPVS. A failing pod is removed in seconds.
7. Shared State — The Hardest Part of Clustering
A cluster is easy until two nodes need to look at the same data. There are three honest options.
7.1 Externalise everything
Move state into Redis / Postgres / S3. Nodes are 100% stateless. Easiest; what every cluster should default to.
7.2 Replicate within the cluster
Each node runs the same store and uses Raft/Paxos to agree on writes. Used by etcd, Consul, CockroachDB, MongoDB replica sets, Kafka.
7.3 Co-locate with the data owner
A "shard" owns a key range. The shard is itself a small HA cluster of 3–5 nodes with its own replication. Used by Cassandra, DynamoDB, ScyllaDB.
⚠️ Rule of thumb: if you can keep your state in someone else's database (RDS, Redis, S3), do. The cluster should be stateless app nodes; the database should be the only stateful component.
8. Failure Modes You Will Hit
| Failure | Symptom | Cure |
|---|---|---|
| Node crash | A request 500s; load balancer health check fails | LB removes it; orchestrator respawns (K8s, systemd, cluster.fork) |
| Slow node (GC, IO) | Health check still passes; latency p99 spikes | Use active health probing (out-of-band RTT), not just "TCP open" |
| Network partition / split-brain | Two nodes both think they're leader | Lease + quorum fencing token; never let two writers touch the same key |
| Cascading failure | One slow downstream takes out the whole cluster | Circuit breakers + bulkheads + request timeouts (start at 100 ms, tune down) |
| Hot key / hot shard | One shard sees 90% of traffic | Cache in front (Redis), add a random suffix to the key, or split by time bucket |
| Stampede / thundering herd | All workers retry the same dead request at once | Exponential backoff with jitter, idempotency keys |
📖 Backoff + jitter: Exponential Backoff deep dive. Idempotency keys: Idempotency Keys deep dive. Circuit breakers: Circuit Breakers deep dive.
9. Cluster Sizing — Back-of-the-Envelope
A useful rule of thumb before you start a project:
| Step | Question | Action |
|---|---|---|
| 1 | What is peak QPS? | Measure or estimate (target ÷ time-of-day curve) |
| 2 | How much CPU per request? | Profile on a single box |
| 3 | How many nodes do I need? | ceil(peak_qps × cpu_per_req / cores_per_node × headroom) — headroom ≥ 2× for failover |
| 4 | How much DB capacity? | peak_qps × row_size × 1 day for hot working set; cache the rest |
| 5 | How much redundancy? | At least N+1, ideally N+2 for HA clusters |
Worked example: 10 000 req/s, 10 ms CPU per request, 8-core nodes.
nodes = ceil(10 000 × 0.010 / 8 × 2) = ceil(25) = 25 nodesThat is, 25 stateless app nodes in steady state — 12 to serve load, 12 to absorb a node failure, 1 to roll during deploys.
10. End-to-End Clustered Architecture (Reference)
Putting every concept together — what a real production cluster looks like.
Layers and their cluster type:
| Layer | Cluster type | Tool |
|---|---|---|
| CDN edge | LB cluster (geo-distributed) | CloudFront, Akamai, Cloudflare |
| App tier | Stateless worker pool | Kubernetes Deployment |
| Cache | Sharded + replicated cluster | Redis Cluster / Memcached |
| Database | HA cluster (primary + replicas) | RDS Multi-AZ, Patroni, Vitess |
| Async workers | Compute cluster (work-stealing) | Kafka consumers, Celery, Sidekiq |
11. Quick Decision Guide
12. TL;DR Checklist
- [ ] I can draw a cluster with LB + workers + shared state.
- [ ] I know the three flavours (load-balancing, HA, compute) and when to use each.
- [ ] I can explain leader election and quorum replication in plain English.
- [ ] I can size a stateless cluster with a back-of-envelope formula.
- [ ] I know the common failure modes (split-brain, hot key, thundering herd) and the one-line cure for each.
- [ ] I can ship a single-process cluster (
cluster.fork) and a multi-machine one (HAProxy). - [ ] I know when to reach for Kubernetes instead of hand-rolling it.
Related pages
- Short version: Clustering Fundamentals
- Load Balancing — the front door of a cluster
- Load Balancing Algorithms
- Fault Tolerance & Crashes — what breaks in a cluster
- Scaling 1 → 1 Billion — cluster growth in practice
- Leader Election, Sharding, Bulkhead — distributed counterparts of cluster patterns
