Skip to content

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 cluster snippet). 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:

  1. Membership — "Who is currently in this cluster, and who is the leader?"
  2. 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

ForceSingle serverCluster
ScalabilityVertical only — buy a bigger boxHorizontal — add another box
AvailabilityOne crash = full outageOne crash = the rest take over
LatencySingle region onlyPlace nodes near users
CostBig iron, fixed capacityCommodity 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:

StrategyHow it worksTrade-off
SynchronousWrite waits for all replicas to ackStrongest consistency, highest latency
AsynchronousWrite returns after primary acks; replicas catch up laterFast writes, risk of data loss on primary crash
Quorum (N of M)Write needs ⌊N/2⌋+1 acksBalanced — 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.

javascript
// 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:

yaml
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: example

haproxy.cfg — Layer-4 round robin with a health check:

text
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 2

Now 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.

yaml
# 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

FailureSymptomCure
Node crashA request 500s; load balancer health check failsLB removes it; orchestrator respawns (K8s, systemd, cluster.fork)
Slow node (GC, IO)Health check still passes; latency p99 spikesUse active health probing (out-of-band RTT), not just "TCP open"
Network partition / split-brainTwo nodes both think they're leaderLease + quorum fencing token; never let two writers touch the same key
Cascading failureOne slow downstream takes out the whole clusterCircuit breakers + bulkheads + request timeouts (start at 100 ms, tune down)
Hot key / hot shardOne shard sees 90% of trafficCache in front (Redis), add a random suffix to the key, or split by time bucket
Stampede / thundering herdAll workers retry the same dead request at onceExponential 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:

StepQuestionAction
1What is peak QPS?Measure or estimate (target ÷ time-of-day curve)
2How much CPU per request?Profile on a single box
3How many nodes do I need?ceil(peak_qps × cpu_per_req / cores_per_node × headroom) — headroom ≥ 2× for failover
4How much DB capacity?peak_qps × row_size × 1 day for hot working set; cache the rest
5How 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 nodes

That 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:

LayerCluster typeTool
CDN edgeLB cluster (geo-distributed)CloudFront, Akamai, Cloudflare
App tierStateless worker poolKubernetes Deployment
CacheSharded + replicated clusterRedis Cluster / Memcached
DatabaseHA cluster (primary + replicas)RDS Multi-AZ, Patroni, Vitess
Async workersCompute 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.

Released under the ISC License.