SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

System DesignHeartbeat & Failure Detection
System Designintermediate

Heartbeat & Failure Detection

Learn how distributed systems detect server failures using Heartbeats. Understand ping-ack protocols, gossip protocols, and build a failure detector in Go.

March 10, 202412 min read
heartbeatfailure-detectiondistributed-systemsreliabilitybuilding-blocktier-4

Metadata

FieldValue
Slugheartbeat-failure-detection
DifficultyIntermediate
Estimated Reading Time15 min
Estimated Coding Time20 min
Tier4 — Reliability & Fault Tolerance
Implementation LanguageGo
SEO DescriptionLearn how Heartbeat mechanisms and Failure Detection work in distributed systems. Understand Ping-Ack, timeouts, phi-accrual, and build a Go failure detector.

1. Overview

What problem does it solve?

In a distributed system with hundreds of servers, servers will crash. Disks will fail, network cables will be unplugged, and kernel panics will occur.

When a server dies, the rest of the system needs to know about it quickly so it can:

  1. Stop routing user traffic to the dead server.
  2. Promote a replica to take over its database duties.
  3. Spin up a replacement server to restore capacity.

A Heartbeat is a periodic signal generated by hardware or software to indicate normal operation. Failure Detection is the algorithm that interprets these heartbeats to decide if a node is truly dead.

What breaks without it?

  • Blackholes: Load balancers keep sending traffic to a dead API server, causing 502 Bad Gateway errors for users.
  • Data Unavailability: A primary database crashes, but because the cluster doesn't realize it's dead, no secondary database is promoted to take its place. The whole app goes down.

2. Motivation

You cannot simply wait for a TCP connection to return an error to know a server is dead. If a server loses power or a network switch dies, it won't send a TCP RST or FIN packet. Requests will just hang indefinitely until they hit a very long timeout (often minutes).

We need a proactive, fast way to detect death. Hence, servers must constantly yell "I'm alive!" every few seconds.


3. Real-World Usage

SystemUse Case
Load Balancers (HAProxy, Nginx)Active health checks (GET /health) every 5 seconds
ZooKeeper / etcdMaintains ephemeral nodes linked to client session heartbeats
CassandraUses a Gossip protocol with a Phi Accrual Failure Detector
ElasticsearchMaster nodes ping data nodes to ensure cluster cohesion
KubernetesKubelet sends heartbeats to the API server; Liveness Probes for Pods

4. Prerequisites

ConceptBlock
HTTP / TCP Basics001 HTTP & TCP Fundamentals
Active Health Checks007 Load Balancer

5. Visual Explanation

The Ping-Ack Protocol (Pull-based)

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcc00', 'edgeLabelBackground':'#ffffff'}}}%%
sequenceDiagram
    participant Monitor as Monitor Node (e.g. Load Balancer)
    participant NodeA as Application Node A
    
    Monitor->>NodeA: Ping (t=0s)
    NodeA-->>Monitor: Ack (I'm healthy!)
    
    Monitor->>NodeA: Ping (t=5s)
    NodeA-->>Monitor: Ack (I'm healthy!)
    
    Note over NodeA: Node A experiences Kernel Panic
    
    Monitor->>NodeA: Ping (t=10s)
    Note over Monitor: Waits 2 seconds for timeout...
    Monitor->>NodeA: Ping (t=12s) - Retry 1
    Note over Monitor: Waits 2 seconds for timeout...
    Monitor->>NodeA: Ping (t=14s) - Retry 2
    Note over Monitor: Waits 2 seconds for timeout...
    
    Note over Monitor: Node A marked DEAD (Evicted from Pool)

The Heartbeat Protocol (Push-based)

Instead of the monitor asking, the nodes proactively broadcast their status.

graph TD
    classDef monitor fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef node fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#155724;
    classDef dead fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#721c24;

    M((Registry / Monitor)):::monitor
    N1((Node 1)):::node
    N2((Node 2)):::node
    N3((Node 3)):::dead
    
    N1 -- "Push Heartbeat (t=10s)" --> M
    N2 -- "Push Heartbeat (t=10s)" --> M
    N3 -. "Silence (Last seen t=2s)" .-> M
    
    Note right of M: Monitor evicts Node 3 at t=12s

6. Internal Working

Types of Heartbeats

  1. Pull (Active Health Checks)

    • The central authority (Load Balancer, API Gateway) pings the nodes.
    • Example: GET /health over HTTP.
    • Pros: Easy to implement.
    • Cons: Doesn't scale well to 10,000+ nodes (Monitor becomes a bottleneck).
  2. Push (Passive Monitoring)

    • Nodes send UDP packets or HTTP requests to a central registry.
    • Pros: Monitor does less work.
    • Cons: If the network to the monitor is saturated, healthy nodes look dead.
  3. Gossip Protocol (Peer-to-Peer)

    • No central monitor. Nodes randomly ping a few other nodes every second. If a node fails to respond, the news is gossiped to the rest of the cluster.
    • Pros: Infinitely scalable. No single point of failure.
    • Cons: Complex to implement. Detection takes slightly longer to propagate.

The Timeout Dilemma

How long should the Monitor wait before declaring a node dead?

  • Too short (e.g., 500ms): A temporary network spike or a GC pause makes the node look dead. The system thrashes, constantly evicting and re-adding nodes (False Positives).
  • Too long (e.g., 30s): User traffic is routed to a black hole for 30 seconds.

Solution: The \Phi (Phi) Accrual Failure Detector Instead of a strict binary (Alive/Dead), calculate the probability a node is dead based on historical response times. If a node usually responds in 10ms but currently hasn't responded in 2 seconds, the suspicion level (\Phi) is very high. This adapts to changing network conditions automatically. (Used by Cassandra).


7. Implementation

Why Go? Go's concurrency model (Goroutines + Tickers) is perfect for building continuous background monitoring loops that don't block the main application.

/*
027 - Heartbeat & Failure Detection
Simulates a central monitor (like a Load Balancer or ZooKeeper)
tracking the health of multiple nodes via Pull-based pinging.
*/
package main

import (
	"fmt"
	"sync"
	"time"
)

// Node Status constants
const (
	StatusHealthy = "HEALTHY"
	StatusSuspect = "SUSPECT" // Missed a ping, but not dead yet
	StatusDead    = "DEAD"
)

// Node represents a server being monitored
type Node struct {
	ID           string
	Address      string
	Status       string
	MissedPings  int
	MaxMisses    int
	IsActuallyUp bool // Simulates the real physical state
}

// Monitor tracks a cluster of nodes
type Monitor struct {
	Nodes map[string]*Node
	mu    sync.Mutex
}

func NewMonitor() *Monitor {
	return &Monitor{
		Nodes: make(map[string]*Node),
	}
}

func (m *Monitor) RegisterNode(id, address string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.Nodes[id] = &Node{
		ID:           id,
		Address:      address,
		Status:       StatusHealthy,
		MissedPings:  0,
		MaxMisses:    3, // Require 3 consecutive misses to declare DEAD
		IsActuallyUp: true,
	}
	fmt.Printf("[Monitor] Registered new node: %s\n", id)
}

// Simulates sending an HTTP GET /health or a TCP Ping
func (m *Monitor) pingNode(node *Node) bool {
	// In reality, this would be an http.Get() with a tight timeout
	// Here we just read the simulated physical state
	return node.IsActuallyUp
}

// StartMonitoring begins the background heartbeat loop
func (m *Monitor) StartMonitoring(interval time.Duration) {
	ticker := time.NewTicker(interval)
	go func() {
		for range ticker.C {
			m.mu.Lock()
			for _, node := range m.Nodes {
				
				// Don't ping dead nodes endlessly in this simple implementation
				if node.Status == StatusDead {
					continue
				}

				success := m.pingNode(node)

				if success {
					if node.Status != StatusHealthy {
						fmt.Printf("[Monitor] 💚 Node %s recovered! Marking HEALTHY.\n", node.ID)
					}
					node.Status = StatusHealthy
					node.MissedPings = 0
				} else {
					node.MissedPings++
					node.Status = StatusSuspect
					fmt.Printf("[Monitor] ⚠️ Node %s missed ping (%d/%d)\n", node.ID, node.MissedPings, node.MaxMisses)

					if node.MissedPings >= node.MaxMisses {
						node.Status = StatusDead
						fmt.Printf("[Monitor] ❌ Node %s declared DEAD. Evicting from pool.\n", node.ID)
						// In real life: Trigger alert, remove from load balancer, initiate failover
					}
				}
			}
			m.mu.Unlock()
		}
	}()
}

// ── Test Harness ──

func main() {
	monitor := NewMonitor()
	
	monitor.RegisterNode("App-Server-1", "10.0.0.1:8080")
	monitor.RegisterNode("App-Server-2", "10.0.0.2:8080")

	// Ping every 500ms
	monitor.StartMonitoring(500 * time.Millisecond)

	time.Sleep(1 * time.Second)
	
	// Simulate Network Cable Unplugged on Server-2
	fmt.Println("\n--- Simulating Network Failure on App-Server-2 ---")
	monitor.mu.Lock()
	monitor.Nodes["App-Server-2"].IsActuallyUp = false
	monitor.mu.Unlock()

	// Wait for Failure Detector to notice and reach MaxMisses
	time.Sleep(2 * time.Second)

	// Simulate Recovery
	fmt.Println("\n--- Simulating Server-2 Reboot & Recovery ---")
	monitor.mu.Lock()
	monitor.Nodes["App-Server-2"].IsActuallyUp = true
	// Must reset status so monitor will ping it again in our naive implementation
	monitor.Nodes["App-Server-2"].Status = StatusSuspect 
	monitor.mu.Unlock()

	time.Sleep(1 * time.Second)
}

Sample Output

[Monitor] Registered new node: App-Server-1
[Monitor] Registered new node: App-Server-2

--- Simulating Network Failure on App-Server-2 ---
[Monitor] ⚠️ Node App-Server-2 missed ping (1/3)
[Monitor] ⚠️ Node App-Server-2 missed ping (2/3)
[Monitor] ⚠️ Node App-Server-2 missed ping (3/3)
[Monitor] ❌ Node App-Server-2 declared DEAD. Evicting from pool.

--- Simulating Server-2 Reboot & Recovery ---
[Monitor] 💚 Node App-Server-2 recovered! Marking HEALTHY.

8. Complexity

MetricPull (Centralized)Push (Centralized)Gossip (P2P)
Network TrafficO(N) per monitorO(N) per monitorO(N \log N) across cluster
Detection TimeFastFastSlightly slower (logarithmic propagation)
Scalability Limit~10,000 nodes~10,000 nodes100,000+ nodes
Single Point of FailureYes (The Monitor)Yes (The Monitor)No

9. Trade-offs

DecisionProsCons
Deep Health Checks (Checking DB connection too)Detects "zombie" nodes where HTTP responds but DB is broken.Expensive. If 50 LBs check 1 node every 2s, the DB gets hammered with 25 queries/sec just for health checks.
Shallow Health Checks (Just return 200)Extremely cheap, high frequency possible.Node might be completely broken internally but still routing traffic.
Short Timeouts / Few RetriesFailover is nearly instant for users.High false positive rate during GC pauses.

10. Production Evolution

FeatureThis ImplementationProduction
State MachineString variableRobust State Machine (INIT -> STARTING -> HEALTHY -> SUSPECT -> DEAD -> DRAINING).
Detection AlgoFixed threshold (3 misses)\Phi Accrual Failure Detector (dynamic thresholds based on network history).
Health EndpointsBoolean returnExposes deep metadata: /health returns { "cpu": "40%", "db_conn": "ok", "version": "1.0.4" }
Split-Brain PreventionNoneIf a node is declared dead, a STONITH (Shoot The Other Node In The Head) command is sent to physically kill power to it, ensuring it doesn't come back online as a "zombie master".

11. Common Bugs

BugWhat happensFix
Cascading Failure due to strict timeoutsCluster gets slightly slow. Heartbeats timeout. Nodes marked dead. Remaining nodes get MORE traffic, get slower, fail heartbeats. Entire cluster evicts itself.Implement adaptive timeouts. If everyone is failing heartbeats, the monitor itself might be partitioned. Don't evict the whole cluster.
Zombie NodesThe main application thread is deadlocked, but a separate lightweight background thread is successfully sending UDP heartbeats.Ensure the heartbeat generation is tied to the actual health of the main application thread.
Network AsymmetryNode A can reach the Monitor, but the Monitor cannot reach Node A.Use bi-directional checks or rely on a consensus quorum.

12. Interview Questions

  1. How does a Load Balancer know when to stop sending traffic to a server? Hint: Active health checks (pinging /health every X seconds) and evicting after Y consecutive failures.

  2. Why do we require 3 consecutive misses to declare a node dead instead of just 1? Hint: Networks are unreliable. Dropped packets are normal. A GC pause might delay a response. We need to prevent false positives and thrashing.

  3. What is a "Zombie" node in failure detection? Hint: A node whose core business logic is deadlocked or broken, but its background health-check endpoint is still blindly returning HTTP 200 OK.

  4. In a 10,000 node cluster, why might a centralized Pull-based monitor fail? Hint: The monitor has to open 10,000 TCP connections every 5 seconds. It runs out of sockets and CPU. Switch to Gossip.


13. Used By (Downstream Blocks)

  • 022 Leader Election — You can't elect a new leader if you don't know the old one is dead.
  • 028 Circuit Breaker — Uses localized failure detection (errors returned) to stop calling a dying downstream service.
  • 033 Monitoring & Alerting — Heartbeats are the foundation of uptime monitoring.

14. Used In (Case Studies)

SystemUse Case
KubernetesKubelet uses Liveness and Readiness probes to monitor Pod health and restart them if they fail.
CassandraUses a Gossip protocol with a Phi-Accrual failure detector to manage cluster topology across datacenters.
ZooKeeper (Kafka)Kafka brokers maintain an ephemeral session with ZooKeeper. If the broker dies, the session times out, and the Controller knows to reassign partitions.

15. Related Blocks

RelationshipBlock
Previous007 Load Balancer
Next028 Circuit Breaker
Alternative037 Gossip Protocol (A specific, decentralized way to do failure detection)

16. Try It Yourself

Exercise 1: \Phi Accrual (Simplified)

Modify the Go implementation to track the LastPingLatency. If the node responds in 10ms normally, but the latest ping takes 500ms, mark it SUSPECT immediately rather than waiting for a full missed ping timeout.

Exercise 2: Cascading Failure Protection

Add a check in the Monitor loop: If more than 50% of the nodes are currently marked SUSPECT or DEAD, log a massive warning "NETWORK PARTITION DETECTED" and temporarily pause evictions to prevent destroying the entire cluster due to a faulty switch.


Website Metadata

FieldValue
Hero TitleHeartbeat & Failure Detection
Hero SubtitleHow distributed systems realize a server has died before users do.
BreadcrumbSystem Design → Building Blocks → Failure Detection
Sidebar CategoryTier 4 — Reliability
Search Keywordsheartbeat, failure detection, ping ack, health check, liveness probe, phi accrual, split brain, distributed systems
Internal Links← 007 Load Balancer · → 028 Circuit Breaker
Suggested IllustrationA doctor taking the pulse of several computer servers with a stethoscope. One server has a flatline on its monitor.
Suggested AnimationA central monitor pinging 3 servers. Server 2 catches fire. The monitor pings it 3 times, gets timeouts, turns the server red, and routes a traffic pipe away from it.
PreviousChecksum & Data IntegrityNextDatabase Replication