KafkaIQ

3.2 ISR & High Watermark Mechanics

Module Overview

Goal: The ONE authoritative explanation of In-Sync Replicas (ISR) and High Watermark (HW) — Kafka's durability boundaries.

What You'll Learn:

  • ISR membership rules and transitions
  • High Watermark calculation algorithm
  • Commit visibility and consumer reads
  • Read consistency guarantees
  • ISR shrinks and expands
  • Edge cases and race conditions

What's NOT Covered Here:

  • Basic replication flow (covered in Module 3.1)
  • Follower fetch details (covered in Module 3.3)
  • Leader election (covered in Module 4.x)

Key Insight: ISR and HW are the core mechanisms that determine what data is "committed" and visible to consumers. Understanding these is critical to understanding Kafka's durability and consistency guarantees.


1. In-Sync Replica Set (ISR)

Definition

ISR = Set of replicas that are "caught up" with the leader

Membership criteria:
└─ Replica has fetched from leader within replica.lag.time.max.ms

Example:
├─ Partition: orders-0
├─ Replicas: [1, 2, 3]
├─ Leader: 1
├─ ISR: [1, 2]  # Broker 3 fell behind
└─ Out-of-sync: [3]

Only ISR members participate in durability guarantees

Why ISR Matters

WITH acks=all:

Producer waits for ISR members (not all replicas):
├─ If ISR = [1, 2, 3]: Wait for all 3
├─ If ISR = [1, 2]: Wait for only 2 (broker 3 slow/dead)
└─ If ISR = [1]: Wait for leader only (dangerous!)

Benefits:
├─ Availability: Slow replicas don't block writes
├─ Durability: Writes are safe on all "healthy" replicas
└─ Flexibility: ISR shrinks/expands dynamically

2. ISR Membership Rules

Joining ISR

FOLLOWER JOINS ISR WHEN:

1. Replica is newly created (starts in ISR)
2. Replica catches up after falling behind

Catch-up criteria:
├─ Follower LEO >= Leader HW
└─ AND replica has fetched within replica.lag.time.max.ms

Process:
1. Follower fetches continuously
2. Follower LEO approaches Leader LEO
3. Leader determines: Follower caught up
4. Leader adds follower to ISR
5. Leader sends AlterPartitionRequest to controller
6. Controller updates cluster metadata
7. ISR change propagated to all brokers

Leaving ISR

FOLLOWER LEAVES ISR WHEN:

Condition:
└─ (now - last fetch time) > replica.lag.time.max.ms

Process:
1. Leader detects: Follower hasn't fetched recently
2. Leader removes follower from ISR (local decision)
3. Leader sends AlterPartitionRequest to controller
4. Controller updates cluster metadata
5. ISR change propagated to all brokers

Reasons for lag:
├─ Follower broker slow (CPU, disk, network)
├─ Follower broker dead
├─ Network partition
└─ Replication too slow (high throughput)

ISR Shrink Example

TIMELINE:

T=0: ISR = [1, 2, 3]

T=5: Broker 3 experiences GC pause (10 seconds)

T=10: Leader detects: Broker 3 last fetch was 10s ago
      replica.lag.time.max.ms = 10000ms (10s)
      Leader decides: Remove broker 3 from ISR
      ISR = [1, 2]

T=15: Broker 3 recovers, resumes fetching

T=20: Broker 3 catches up (LEO >= HW)
      Leader decides: Add broker 3 back to ISR
      ISR = [1, 2, 3]

Result:
└─ Temporary ISR shrink during issue
└─ Automatic recovery when caught up

3. Configuration: replica.lag.time.max.ms

Critical Parameter

replica.lag.time.max.ms = 10000  # Default: 10 seconds

Meaning:
└─ Max time a follower can go without fetching before being removed from ISR

Impact:

Too low (e.g., 1000ms):
├─ Frequent ISR shrinks (transient issues trigger removal)
├─ ISR instability
└─ May reduce availability (fewer ISR members)

Too high (e.g., 60000ms):
├─ Slow followers remain in ISR longer
├─ acks=all waits longer for slow replicas
└─ Reduced write throughput

Recommended:
└─ 10-30 seconds (balance stability vs responsiveness)

4. High Watermark (HW)

Definition

High Watermark (HW):
└─ Offset of the last record fully replicated to all ISR members
└─ Records up to HW are "committed" and visible to consumers

Calculation:
└─ HW = min(LEO of all ISR members)

Example:
├─ Leader LEO: 1010
├─ Follower 1 LEO: 1009
├─ Follower 2 LEO: 1008
├─ ISR: [Leader, Follower 1, Follower 2]
└─ HW: 1008 (minimum of ISR LEOs)

HW Update Algorithm

LEADER UPDATES HW:

After each follower fetch:
1. Update follower's LEO (from FetchRequest offset)
2. Recalculate HW = min(all ISR member LEOs)
3. If HW advanced:
   ├─ Update leader's local HW
   └─ Include new HW in next FetchResponse to followers

Followers update HW:
└─ Upon receiving FetchResponse with HW
└─ Set local HW = leader's HW (from response)

HW Advancement Example

TIMELINE:

T=0:
├─ Leader appends records 1000-1010
├─ Leader LEO: 1010
├─ Follower 1 LEO: 999
├─ Follower 2 LEO: 998
└─ HW: 999 (min of ISR LEOs)

T=1: Follower 1 fetches
├─ Follower 1 LEO: 1010 (caught up)
├─ Leader recalculates: HW = min(1010, 1010, 998) = 998
└─ HW: 998 (no change yet)

T=2: Follower 2 fetches
├─ Follower 2 LEO: 1010 (caught up)
├─ Leader recalculates: HW = min(1010, 1010, 1010) = 1010
└─ HW: 1010 (advanced!)

Result:
└─ Records 999-1009 now visible to consumers

5. Consumer Visibility

Read-Your-Writes Semantics

CONSUMERS CAN ONLY READ UP TO HW:

Partition state:
├─ Leader LEO: 1010
├─ HW: 1008
└─ Records 1008, 1009 not yet committed

Consumer fetch at offset 1005:
├─ Can read: 1005, 1006, 1007
├─ Cannot read: 1008, 1009 (beyond HW)
└─ Fetch returns records 1005-1007, HW=1008

Why?
├─ Records beyond HW not yet fully replicated
├─ May be lost if leader fails
└─ Ensuring consumers see only "committed" data

Read Consistency Guarantee

KAFKA'S CONSISTENCY MODEL:

Read Committed Isolation:
└─ Consumers see only records up to HW
└─ HW = committed data
└─ Beyond HW = uncommitted (not visible)

Consistency:
├─ All consumers see same view (up to HW)
├─ Reads are consistent across replicas
└─ No dirty reads (uncommitted data)

Trade-off:
├─ Slightly stale reads (lag between LEO and HW)
└─ But strong consistency (committed data)

6. ISR and min.insync.replicas

Durability Configuration

min.insync.replicas = 2  # Broker or topic config

Meaning:
└─ Minimum ISR size required for writes to succeed

With acks=all:
├─ Producer write requires ISR size >= min.insync.replicas
├─ If ISR < min.insync.replicas:
│   └─ Producer receives NOT_ENOUGH_REPLICAS error
└─ Write rejected (not durable enough)

Example:
├─ Replicas: [1, 2, 3]
├─ ISR: [1] (brokers 2 and 3 down)
├─ min.insync.replicas: 2
└─ Write fails: NOT_ENOUGH_REPLICAS

Availability vs Durability

CONFIGURATION TRADE-OFF:

min.insync.replicas = 1:
├─ Writes succeed with only leader (ISR=[1])
├─ High availability (tolerates all followers down)
└─ Low durability (data on one replica)

min.insync.replicas = 2:
├─ Writes require leader + 1 follower (ISR=[1,2])
├─ Moderate availability (tolerates 1 follower down)
└─ High durability (data on 2 replicas)

min.insync.replicas = 3:
├─ Writes require all replicas (ISR=[1,2,3])
├─ Low availability (any failure blocks writes)
└─ Highest durability (data on 3 replicas)

Recommended:
└─ RF=3, min.insync.replicas=2
└─ Balances availability and durability

7. ISR Shrinks and Expands

ISR Shrink Metrics

JMX METRIC:

kafka.server:type=ReplicaManager,name=IsrShrinksPerSec

Meaning:
└─ Rate of ISR membership decreases

High shrink rate indicates:
├─ Followers frequently falling behind
├─ replica.lag.time.max.ms too aggressive
├─ Broker capacity issues (CPU, disk, network)
└─ Need to investigate and fix root cause

Monitoring:
└─ Alert if IsrShrinksPerSec > threshold (e.g., 10/min)

ISR Expand Metrics

JMX METRIC:

kafka.server:type=ReplicaManager,name=IsrExpandsPerSec

Meaning:
└─ Rate of ISR membership increases

High expand rate (after shrinks):
├─ Followers catching up after falling behind
├─ Recovery from temporary issues
└─ Positive sign (system healing)

Pattern:
└─ Shrinks followed by expands = transient issues
└─ Shrinks without expands = persistent failure

8. Under-Replicated Partitions

Definition

UNDER-REPLICATED PARTITION:

Condition:
└─ ISR size < Replication Factor

Example:
├─ Replicas: [1, 2, 3] (RF=3)
├─ ISR: [1, 2] (broker 3 out-of-sync)
└─ Under-replicated: Yes

Causes:
├─ Follower lagging
├─ Broker down
├─ Network issues
└─ Insufficient replication capacity

Impact:
└─ Reduced fault tolerance (fewer in-sync replicas)

Monitoring

JMX METRIC:

kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions

Meaning:
└─ Number of partitions where ISR < RF

Target: 0
└─ Any non-zero value = problem

Alerts:
├─ Immediate alert if > 0
├─ Investigate which partitions affected
└─ Identify root cause (slow broker, network, etc.)

9. HW Truncation

Why Truncation is Needed

SCENARIO: Leader Failover

Before failover:
├─ Old leader LEO: 1010
├─ Follower A LEO: 1008
├─ Follower B LEO: 1009
├─ HW: 1008 (committed)

Old leader dies:
└─ Follower A becomes new leader

Issue:
├─ Old leader had records 1009 (uncommitted)
├─ New leader (Follower A) doesn't have 1009
└─ If old leader rejoins, 1009 is divergent

Solution:
└─ Old leader truncates to HW (1008) upon rejoining
└─ Discards uncommitted record 1009

Truncation Process

WHEN FOLLOWER REJOINS AFTER LEADER CHANGE:

1. Follower fetches from new leader
2. New leader responds with:
   ├─ Diverging epoch (leader epoch changed)
   └─ New HW

3. Follower detects epoch mismatch
   ├─ Realizes: Leader changed while I was offline
   └─ Fetches OffsetsForLeaderEpoch from new leader

4. New leader returns: Safe truncation offset

5. Follower truncates local log to safe offset
   └─ Discards any divergent records

6. Follower resumes replication from truncation point

10. Leader Epoch

Purpose

LEADER EPOCH:

Definition:
└─ Counter incremented each time partition leader changes

Purpose:
└─ Detect leader changes
└─ Determine safe truncation point

Structure:
├─ Epoch: Integer counter (0, 1, 2, ...)
├─ Start offset: First offset for this epoch
└─ Stored in: leader-epoch-checkpoint file

Epoch-Based Truncation

USING EPOCHS:

Old leader log:
├─ Epoch 0: Offsets 0-999
├─ Epoch 1: Offsets 1000-1050 (old leader)
└─ LEO: 1051

New leader log:
├─ Epoch 0: Offsets 0-999
├─ Epoch 2: Offsets 1000-1040 (new leader, different epoch!)
└─ LEO: 1041

Old leader rejoins:
1. Fetches OffsetsForLeaderEpoch(epoch=1)
2. New leader responds: "Epoch 1 doesn't exist, truncate to 1000"
3. Old leader truncates offsets 1000-1050
4. Old leader starts replicating from offset 1000 (epoch 2)

11. Key Takeaways

You've learned:

✅ ISR = replicas caught up within replica.lag.time.max.ms
✅ High Watermark = min(ISR member LEOs) — last committed offset
✅ Consumers read up to HW — committed data only
✅ ISR shrinks when followers lag or fail
✅ ISR expands when followers catch up
✅ min.insync.replicas controls durability threshold
✅ Under-replicated partitions = ISR < RF (needs investigation)
✅ HW truncation resolves divergence after leader change
✅ Leader epochs enable safe truncation
✅ ISR metrics (shrinks, expands, under-replicated) critical for monitoring


Summary

ISR and HW define Kafka's durability boundaries:

ISR Mechanics =
├─ Dynamic membership (shrinks/expands)
├─ Based on replica.lag.time.max.ms
├─ Only ISR members participate in acks=all
└─ Protects availability (slow replicas don't block)

High Watermark =
├─ HW = min(ISR member LEOs)
├─ Advances as followers catch up
├─ Defines "committed" data
└─ Consumer visibility boundary

Durability Configuration =
├─ RF=3, min.insync.replicas=2 (recommended)
├─ Balances availability and durability
├─ NOT_ENOUGH_REPLICAS if ISR too small
└─ Prevents under-replicated writes

Truncation =
├─ Leader epochs detect leader changes
├─ Followers truncate divergent data
└─ Ensures log consistency after failover

Remember: ISR and HW are the heart of Kafka's replication guarantees. Understanding these mechanics is essential for designing durable and available Kafka systems.

Next up: Module 3.3 — Follower Fetching & Data Consistency — Deep dive into follower behavior.