Skip to content
LinkState
Go back

VIP Failover Without Session Ownership

Introduction to Stateful Load Balancers and Outage Scenarios

Overview of Stateful Load Balancing

Stateful load balancers (SLBs) maintain per‑connection or per‑client state so that subsequent packets from the same flow are directed to the same backend server. Typical mechanisms include:

The state is usually kept in memory on the LB node; in an HA pair it must be replicated to the standby node so that a failover does not break existing flows.

Importance of Session Persistence

Many applications rely on sticky sessions: shopping carts, authentication tokens, in‑process caches, or any server‑side state that is not shared across backend nodes. If persistence is lost after a failover, a client that had an established session will be sent to a different backend on the next request, causing:

From the network perspective the VIP remains reachable, health checks to the backend pool continue to succeed, and routing tables show a valid path—yet the user experience degrades. This mismatch creates three divergent narratives: routing success, health‑check green, and user‑visible breakage.


Reconstructing the Outage

Initial Failover and VIP Reachability

At 02:13 UTC a core switch port flapped, triggering VRRP failover of the active‑passive LB pair (lb01lb02). The virtual IP (10.0.0.100/24) remained owned by lb02; ARP replies and ICMP echo replies continued without interruption.

# Before failover
$ ping -c 3 10.0.0.100
64 bytes from 10.0.0.100: icmp_seq=1 ttl=64 time=0.3 ms
64 bytes from 10.0.0.100: icmp_seq=2 ttl=64 time=0.3 ms
64 bytes from 10.0.0.100: icmp_seq=3 ttl=64 time=0.3 ms

# Immediately after VRRP transition (no packet loss)
$ ping -c 3 10.0.0.100
64 bytes from 4.0.0.100: icmp_seq=1 ttl=10.0.0.100: icmp_seq=1 ttl=64 time=0.3 ms
64 bytes from 10.0.0.100: icmp_seq=2 ttl=64 time=0.3 ms
64 bytes from 10.0.0.100: icmp_seq=3 ttl=64 time=0.3 ms

Loss of Session Ownership Loss of Session Ownership and Its Implications

The LB pair used VRRP only for HA; state synchronization was disabled (a common oversight when migrating from a legacy active‑active setup). Consequently, the stick‑table on lb01 was not replicated to lb02. When lb02 took over the VIP, its stick‑table was empty. New packets belonging to existing flows were load‑balanced using the default algorithm (round‑robin) instead of being pinned to the original backend.

Technical Details of the Outage

Time (UTC)EventObservation
02:12:58Core switch port down (iface eth1/24)Link‑loss logged on switch
02:13:01VRRP priority change on lb01 (100 → 50)lb01 logs: VRRP: State change BACKUP → INIT
02:13:02lb02 assumes MASTER state, takes VIPlb02 logs: VRRP: State change BACKUP → MASTER
02:13:03First client POST after failover arrives at lb02LB logs: stick-table lookup miss → fallback to round-robin
02:13:04Backend web01 returns HTTP 302 to /loginApplication logs: Session not found for cookie=abc123
02:13:05‑02:15:00Surge of login redirects, increased auth‑server loadAuth server metrics: login requests ×5
02:15:00Operator notices spike in 401/302 responsesAlert fires on HTTP error rate >5%
02:15:10Manual failback to lb01 (after state sync re‑enabled)VIP returns to lb01, stick‑table repopulated from peer, sessions restored

The root cause was the absence of stick‑table replication (peers section missing or disabled) in the HAProxy configuration.


Troubleshooting the Outage

Identifying Symptoms and Gathering Logs

  1. User‑facing metrics – spike in**02/401, increase in average response time.
  2. LB logs – repeated stick-table lookup miss messages.
  3. Network traces – TCP SYN‑ACK from VIP to varying backend IPs for the same client IP.
  4. VRRP logs – clean master transition, no split‑brain.

Log collection commands (example on Linux‑based HAProxy):

# Collect recent HAProxy logs
$ sudo journalctl -u haproxy --since "2025-09-24 02:10:00" > haproxy_before_failover.log

# Capture VRRP state changes
$ sudo grep VRRP /var/log/syslog | grep -E "2025-09-24 02:1[0-9]" > vrrp_transitions.log

# Dump stick-table at intervals (requires stats socket)
$ echo "show table http-stick" | sudo socat stdio /var/run/haproxy.sock > sticktable_02_13_00.txt
$ echo "show table http-stick" | sudo socat stdio /var/run/haproxy.sock > sticktable_02_13_05.txt

Analyzing Network Traffic and Load Balancer Configurations

$ sudo tcpdump -i eth0 -nn -s 0 -w /tmp/lb_failover.pcap 'dst port 80 or dst port 443' &
# After a few minutes, stop capture and examine:
$ sudo tshark -r /tmp/lb_failover.pcap -Y "tcp.flags.syn==1 && ip.src==10.0.0.100" -T fields -e ip.dst | sort | uniq -c

Output showed a roughly equal distribution across web01–web04, confirming loss of persistence.

$ sudo diff -u /etc/haproxy/haproxy.cfg.lb01 /etc/haproxy/haproxy.cfg.lb02
--- haproxy.cfg.lb01 2025-09-20 12:00:00
+++ haproxy.cfg.lb02 2025-09-20 12:00:00
@@
-    peers haproxy_peers
-        peer lb01 10.0.0.10:1024
-        peer lb02 10.0.0.11:1024
+    # peers section disabled for testing

The peers block was commented out on lb02, meaning no state sync.

Code Examples for Debugging Load Balancer Issues

CLI Commands for Load Balancer Inspection

PurposeCommand (HAProxy)Example Output
Show stick‑table contents`echo “show table http-stick”socat stdio /var/run/haproxy.sock`
List backend servers and their state`echo “show backend”socat stdio /var/run/haproxy.sock`
Dump runtime stats in JSON (for automation)`echo “show info json”socat stdio /var/run/haproxy.sock

Scripting for Automated Troubleshooting

A simple Bash script that polls the stick‑table size and alerts if it drops below a threshold (indicating loss of replication):

#!/usr/bin/env bash
SOCKET="/var/run/haproxy.sock"
THRESHOLD=1000   # expect at least 1k entries under normal load
INTERVAL=15      # seconds

while true; do
    SIZE=$(echo "show table http-stick" | socat stdio "$SOCKET" | awk '/^Table:/ {print $2}')
    if [[ -z "$SIZE" || "$SIZE" -lt "$THRESHOLD" ]]; then
        logger -t haproxy-stick "ALERT: stick-table size $SIZE (< $THRESHOLD)"
        # optional: trigger pagerduty/webhook
    fi
    sleep $INTERVAL
done

Run as a systemd service to get early‑warning continuous visibility.


Routing Success and Health Checks

Understanding Routing Success in the Context of the Outage

Health Check Configurations and Their Role in the Outage

Health checks were configured as layer‑7 HTTP GET to /health on each backend, performed every 5 seconds with a rise of 2 and fall of 3. Because each check established a new TCP connection, the LB treated them as independent flows and selected a backend based on the current algorithm (round‑robin). The backend pool was healthy, so all checks passed, giving a false sense of normality.

HAProxy health‑check snippet (present on both nodes):

backend web_pool
    mode http
    balance roundrobin          # <-- used for health checks when stick-table miss
    option httpchk GET /health
    http-check expect status 200
    server web01 10.0.1.10:80 check inter 5s rise 2 fall 3
    server web02 10.0.1.11:80 check inter 5s rise 2 fall 3
    server web03 10.0.1.12:80 check inter 5s rise 2 fall 3
    server web04 10.0.1.13:80 check inter 5s rise 2 fall 3

If the stick-table had been populated, the health‑check traffic would still have been load‑balanced, but the user traffic would have been persisted. The key point: health‑check success does not guarantee session persistence.

Example Code for Configuring Health Checks


Share this post on:

Previous Post
Microbursts, NAPI Budget, and Fast-Path Tradeoffs
Next Post
Refusal behavior needs regression gates too