Loading learning content...
TCP Reno, released in 1990 as part of 4.3BSD Reno, marked a watershed moment in the evolution of the Transmission Control Protocol. By incorporating Van Jacobson's Fast Retransmit and Fast Recovery algorithms, Reno transformed TCP from a protocol that crumbled under packet loss into one that could maintain reasonable performance in real-world network conditions.
Understanding Reno's implementation is essential for several reasons: it remains the baseline against which all subsequent TCP variants are compared, its algorithms are still active in many deployed systems, and its limitations motivated the development of every major TCP improvement since 1990. This page provides a detailed examination of Reno's Fast Recovery implementation, its operational characteristics, and the lessons learned from its deployment.
This page provides comprehensive coverage of TCP Reno's Fast Recovery implementation, including: the complete state machine and algorithm specification, code-level implementation details, known limitations and edge cases, the evolution from Reno to NewReno and beyond, and best practices for modern TCP deployments.
TCP Reno's Fast Recovery is defined by a precise sequence of state transitions and window adjustments. The algorithm, later formalized in RFC 5681, consists of three phases: entry, maintenance, and exit.
Phase 1: Entry (On Third Duplicate ACK)
When the sender receives the third duplicate ACK for the same sequence number:
Phase 2: Maintenance (During Fast Recovery)
For each additional duplicate ACK received:
Phase 3: Exit (On New ACK)
When an ACK acknowledges new data:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// TCP Reno Fast Recovery - RFC 5681 Section 3.2 STATE VARIABLES: cwnd : congestion window (bytes) ssthresh : slow start threshold (bytes) FlightSize : bytes currently in flight (SND.NXT - SND.UNA) dupACKcount : count of duplicate ACKs received state : current TCP state (OPEN, RECOVERY, etc.) recover : highest sequence number at FR entry // Event: Duplicate ACK receivedON dupACK: dupACKcount = dupACKcount + 1 IF dupACKcount = 3: // Enter Fast Recovery ssthresh = max(FlightSize / 2, 2 * MSS) retransmit(SND.UNA) cwnd = ssthresh + 3 * MSS recover = SND.NXT - 1 state = FAST_RECOVERY ELSE IF state = FAST_RECOVERY AND dupACKcount > 3: // Window inflation cwnd = cwnd + MSS // Transmit new data if possible IF cwnd - FlightSize >= MSS: transmit_new() // Event: New ACK receivedON newACK: IF state = FAST_RECOVERY: IF ack_seq > recover: // Full recovery complete cwnd = ssthresh dupACKcount = 0 state = CONGESTION_AVOIDANCE ELSE: // Partial ACK (Reno behavior: exit and re-enter via dup ACKs) // Note: This is a known limitation cwnd = ssthresh dupACKcount = 0 state = CONGESTION_AVOIDANCE // Will re-enter FR when dup ACKs arrive for this partial ACK ELSE: // Normal ACK processing dupACKcount = 0 IF state = SLOW_START AND cwnd < ssthresh: cwnd = cwnd + MSS // Exponential growth ELSE: cwnd = cwnd + MSS * MSS / cwnd // Linear growth // Event: Retransmission timeoutON RTO: ssthresh = max(FlightSize / 2, 2 * MSS) cwnd = MSS // Or initial window dupACKcount = 0 state = SLOW_START retransmit(SND.UNA)RFC 5681 codifies the 'modern' understanding of Fast Recovery with some clarifications compared to the original 4.3BSD Reno implementation. Key differences include more precise FlightSize calculation and explicit specification of the recover variable for tracking recovery progress.
Implementing TCP Reno's Fast Recovery correctly requires attention to numerous details that aren't immediately obvious from the high-level algorithm description.
Duplicate ACK Detection:
A duplicate ACK is precisely defined as an ACK that:
Implementations must carefully filter ACKs to avoid false duplicate detection.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
/* TCP Reno Fast Recovery Implementation */ typedef struct { uint32_t snd_una; /* Oldest unacked sequence */ uint32_t snd_nxt; /* Next sequence to send */ uint32_t cwnd; /* Congestion window (bytes) */ uint32_t ssthresh; /* Slow start threshold */ uint32_t mss; /* Maximum segment size */ uint32_t recover; /* Recovery point */ uint16_t dup_ack_cnt; /* Duplicate ACK counter */ uint8_t state; /* Connection state */} tcp_conn_t; #define TCP_STATE_OPEN 0#define TCP_STATE_FAST_RECOVERY 1#define TCP_STATE_SLOW_START 2#define TCP_STATE_CONG_AVOID 3 /* Check if ACK is a duplicate */bool is_duplicate_ack(tcp_conn_t *conn, tcp_header_t *ack) { /* ACK number matches last received acknowledgment */ if (ack->ack_num != conn->snd_una) return false; /* No data, SYN, or FIN */ if (ack->data_len > 0 || (ack->flags & (TH_SYN | TH_FIN))) return false; /* No window update */ if (ack->window != conn->last_rcv_wnd) return false; /* There must be outstanding data */ if (conn->snd_una == conn->snd_nxt) return false; return true;} /* Process incoming ACK */void tcp_reno_process_ack(tcp_conn_t *conn, tcp_header_t *ack) { uint32_t flight_size = conn->snd_nxt - conn->snd_una; if (is_duplicate_ack(conn, ack)) { /* Duplicate ACK processing */ conn->dup_ack_cnt++; if (conn->dup_ack_cnt == 3) { /* Enter Fast Recovery */ /* Calculate new threshold */ conn->ssthresh = MAX(flight_size / 2, 2 * conn->mss); /* Fast Retransmit */ tcp_retransmit(conn, conn->snd_una); /* Set cwnd with inflation for 3 dup ACKs */ conn->cwnd = conn->ssthresh + 3 * conn->mss; /* Record recovery point */ conn->recover = conn->snd_nxt - 1; conn->state = TCP_STATE_FAST_RECOVERY; TCP_LOG("Entering FR: ssthresh=%u cwnd=%u recover=%u", conn->ssthresh, conn->cwnd, conn->recover); } else if (conn->state == TCP_STATE_FAST_RECOVERY && conn->dup_ack_cnt > 3) { /* Window inflation during Fast Recovery */ conn->cwnd += conn->mss; /* Try to send new data */ tcp_reno_send_if_possible(conn); } } else if (ack->ack_num > conn->snd_una) { /* New ACK - advances sequence */ uint32_t acked_bytes = ack->ack_num - conn->snd_una; conn->snd_una = ack->ack_num; if (conn->state == TCP_STATE_FAST_RECOVERY) { if (ack->ack_num > conn->recover) { /* Full recovery complete */ conn->cwnd = conn->ssthresh; conn->dup_ack_cnt = 0; conn->state = TCP_STATE_CONG_AVOID; TCP_LOG("FR complete: cwnd=%u", conn->cwnd); } else { /* Partial ACK - Reno exits FR (this is a limitation!) */ conn->cwnd = conn->ssthresh; conn->dup_ack_cnt = 0; conn->state = TCP_STATE_CONG_AVOID; TCP_LOG("Partial ACK: exiting FR prematurely"); } } else { /* Normal ACK processing */ conn->dup_ack_cnt = 0; if (conn->state == TCP_STATE_SLOW_START && conn->cwnd < conn->ssthresh) { /* Slow start: exponential growth */ conn->cwnd += conn->mss; } else { /* Congestion avoidance: linear growth */ conn->cwnd += (conn->mss * conn->mss) / conn->cwnd; } } }} /* Send new data if window allows */void tcp_reno_send_if_possible(tcp_conn_t *conn) { uint32_t flight_size = conn->snd_nxt - conn->snd_una; while (conn->cwnd - flight_size >= conn->mss) { if (tcp_has_data_to_send(conn)) { tcp_send_segment(conn, conn->snd_nxt, conn->mss); conn->snd_nxt += conn->mss; flight_size += conn->mss; } else { break; } }}Common implementation errors include: not checking for window updates when detecting duplicates, forgetting to reset dup_ack_cnt on new ACKs, incorrect flight_size calculation, and failure to handle wrapping sequence numbers. Each of these can cause subtle, hard-to-debug performance issues.
Despite its transformative impact, TCP Reno has well-documented limitations that motivated the development of improved variants. Understanding these limitations is essential for both historical perspective and practical troubleshooting.
Limitation 1: Multiple Losses in a Single Window
Reno's most significant limitation is its poor handling of multiple lost segments within a single window of data. When multiple segments are lost:
This pattern can repeat for each lost segment, drastically reducing ssthresh with each iteration.
| Event | ssthresh | cwnd | State | Problem |
|---|---|---|---|---|
| Initial state | ∞ | 100 | CA | — |
| Loss of seg 5 & 7 detected | 50 | 53 | FR | First reduction |
| Seg 5 retransmit ACKed (partial) | 50 | 50 | CA | Premature exit |
| 3 dup ACKs for seg 7 | 25 | 28 | FR | Second reduction! |
| Seg 7 retransmit ACKed | 25 | 25 | CA | Severely reduced |
Limitation 2: Timeout After Fast Recovery
If the retransmitted segment is also lost (or if more than one window worth of data is lost), Fast Recovery cannot complete successfully. Eventually, RTO expires:
Limitation 3: False Fast Retransmit
Network reordering can cause three duplicate ACKs without actual packet loss:
Limitation 4: No SACK Synergy
Reno was designed before SACK was standardized. Without SACK information:
The multiple-loss limitation is particularly severe because high-speed networks often experience bursty loss (router tail-drop). If a burst causes 3 segments to be lost in one window, Reno may reduce ssthresh by 87.5% (three halvings) instead of the intended 50%. This causes persistent under-utilization.
TCP NewReno (RFC 6582) was developed specifically to address Reno's multiple-loss limitation. The key insight was simple but effective: don't exit Fast Recovery on partial ACKs.
NewReno's Partial ACK Handling:
When NewReno receives a partial ACK (ACK that advances but doesn't acknowledge all data up to the recovery point):
This approach allows recovery from multiple losses without multiple ssthresh halvings.
12345678910111213141516171819202122232425262728293031323334353637
// NewReno Partial ACK Handling (RFC 6582) // In Fast Recovery, on receiving a new ACK:ON newACK during FAST_RECOVERY: IF ack_seq > recover: // Full recovery - same as Reno cwnd = ssthresh exit FAST_RECOVERY enter CONGESTION_AVOIDANCE ELSE: // Partial ACK - NewReno improvement // 1. Immediately retransmit the next lost segment retransmit(ack_seq) // Segment starting at new ack // 2. Deflate window by acknowledged data acked_bytes = ack_seq - previous_snd_una cwnd = cwnd - acked_bytes // 3. Add back 1 MSS (segment left network, triggering this ACK) cwnd = cwnd + MSS // 4. Remain in Fast Recovery (key difference from Reno!) // Do NOT reset dup_ack_cnt // Do NOT exit Fast Recovery // 5. Continue sending if window allows IF cwnd - FlightSize >= MSS: transmit_new() // Stay in FAST_RECOVERY state // Benefits:// - Single ssthresh halving for entire loss event// - Multiple losses recovered in same FR period// - Faster overall recoveryNewReno Performance Comparison:
Consider the same two-loss scenario from earlier:
Reno:
NewReno:
NewReno became the recommended TCP variant and is the minimum expected behavior in modern implementations. RFC 6582 (originally RFC 3782) provides the authoritative specification. Most operating systems have implemented NewReno or better since the early 2000s.
While NewReno addressed multiple-loss recovery, SACK-based recovery (RFC 6675) provides even more efficient loss recovery by leveraging Selective Acknowledgment information.
The SACK Advantage:
Without SACK, the sender must infer which segments are lost based on duplicate ACKs and partial ACKs. With SACK, the receiver explicitly reports which segments have been received out of order:
SACK Block: 3001-4000, 5001-6000
This tells the sender: 'I have bytes 3001-4000 and 5001-6000, but I'm missing 1001-3000 and 4001-5000.'
SACK Recovery Mechanics:
With SACK information, the sender can:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// SACK-Based Recovery (RFC 6675) DATA STRUCTURES: SCOREBOARD: tracks which segments are (a) sent, (b) SACKed, (c) lost // Enter Fast Recovery (enhanced for SACK)ON third duplicate ACK with SACK info: ssthresh = max(FlightSize/2, 2*MSS) cwnd = ssthresh + 3*MSS recover = SND.NXT // Initialize scoreboard from cumulative ACK update_scoreboard(sack_blocks) // Mark head segment as lost mark_as_lost(SND.UNA) enter FAST_RECOVERY // Calculate "pipe" (RFC 6675 term for bytes in flight)FUNCTION calculate_pipe(): pipe = 0 FOR each segment in send buffer: IF segment is sent AND NOT SACKed AND NOT marked_lost: pipe = pipe + segment.size IF segment is marked_lost AND NOT yet retransmitted: // Lost segment uses network capacity too pipe = pipe + segment.size RETURN pipe // Sending during SACK-based recoveryON opportunity_to_send: pipe = calculate_pipe() WHILE cwnd - pipe >= MSS: // Priority 1: Retransmit lost segments IF exists segment marked_as_lost AND NOT retransmitted: retransmit(earliest_lost_segment) pipe = pipe + MSS CONTINUE // Priority 2: Send new data IF exists unsent data: send_new_segment() pipe = pipe + MSS ELSE: BREAK // Update scoreboard on each ACKON ACK with SACK blocks: FOR each SACK block: mark_segments_as_SACKed(block.start, block.end) // RFC 6675: Segment is considered lost if 3+ segments // after it have been SACKed (similar to 3 dup ACK threshold) FOR each segment: IF segment is NOT SACKed AND 3+ later segments ARE SACKed: mark_as_lost(segment)SACK vs. NewReno Performance:
Consider a scenario with 3 non-consecutive losses in a window of 20 segments:
NewReno:
SACK Recovery:
The difference grows with the number of losses: N losses require N RTTs with NewReno but potentially 1-2 RTTs with SACK.
SACK is enabled by default in virtually all modern TCP implementations. It's negotiated during the three-way handshake via TCP options. If both sides support SACK, SACK-based recovery is used; otherwise, the implementation falls back to NewReno-style recovery.
The decades since Reno have seen continuous evolution in TCP congestion control. Each variant addresses specific limitations while maintaining the fundamental principles Reno established.
The Evolution Tree:
TCP Tahoe (1988)
↓
TCP Reno (1990) ← First Fast Recovery
↓
TCP NewReno (1999) ← Better partial ACK handling
↓
├── SACK Recovery (2000s) ← Selective acknowledgment
├── TCP Vegas (1994) ← Delay-based congestion detection
├── BIC (2004) ← Binary search for optimal rate
├── CUBIC (2008) ← Standard in Linux
├── Compound TCP (2006) ← Microsoft's approach
└── BBR (2016) ← Google's model-based CC
| Variant | Key Innovation | Impact on Fast Recovery | Current Status |
|---|---|---|---|
| NewReno | Partial ACK handling | Single FR per loss event | Minimum standard |
| SACK | Explicit loss info | Parallel retransmission | Nearly universal |
| BIC/CUBIC | Aggressive window growth | Faster post-FR recovery | Linux default |
| Compound | Delay-based component | Earlier congestion detection | Windows default |
| BBR | Rate estimation model | May not use FR (loss-insensitive) | Widely deployed |
CUBIC's Approach:
TCP CUBIC modifies Fast Recovery in two key ways:
This allows CUBIC to recover lost bandwidth faster on high-BDP networks while maintaining fairness.
BBR's Different Philosophy:
BBR (Bottleneck Bandwidth and Round-trip propagation time) takes a fundamentally different approach:
BBR still implements Fast Recovery for wire compatibility but may not reduce its rate estimate on isolated losses, relying instead on its bandwidth model.
Despite decades of evolution, Reno's core principles remain: • Differentiate response based on congestion signal type • Use duplicate ACKs for fast loss detection • Maintain window inflation during recovery for continued flow • Avoid expensive slow start when evidence suggests isolated loss
Every modern variant is, in a sense, an optimization of Reno's fundamental approach.
While Reno itself is rarely the best choice for modern deployments, understanding its principles informs proper TCP configuration. Here are best practices for Fast Recovery and related mechanisms.
Implementation Selection:
For new deployments, consider:
SACK Configuration:
Ensure SACK is enabled on both endpoints:
sysctl net.ipv4.tcp_sack (default: 1)Debugging Fast Recovery Issues:
When diagnosing TCP performance problems:
Check for Multiple Loss: If throughput is much lower than expected, look for multiple-loss events causing repeated ssthresh halvings.
Verify SACK Negotiation: Use tcpdump/Wireshark to confirm SACK is being used.
Watch for Spurious Recovery: DSACK can reveal unnecessary retransmissions due to reordering.
Examine CCState: Modern tools (like Linux's ss command) can show current congestion control state.
# Linux: Check TCP congestion control state
ss -ti | grep -A 5 "your_connection"
# Shows: cwnd, ssthresh, rtt, retrans, etc.
If you observe pure Reno behavior (multiple ssthresh halvings for one congestion event), investigate: (1) SACK might be disabled or stripped by middleboxes, (2) older TCP stack without NewReno, (3) embedded/IoT devices with limited TCP implementation. Upgrading or reconfiguring can significantly improve performance.
TCP Reno's implementation of Fast Recovery established the foundation for all modern TCP congestion control. Let's consolidate the key concepts from this page and the entire module:
Module 2: Fast Recovery — Complete
Throughout this module, we've explored:
You now possess comprehensive knowledge of TCP Fast Recovery—from theoretical foundations to implementation details to practical configuration. This knowledge is fundamental for network engineering, performance optimization, and understanding the behavior of countless TCP connections carrying the world's internet traffic.
Congratulations! You have completed the Fast Recovery module. You now understand one of TCP's most important performance optimizations—how it enables efficient loss recovery without the devastating throughput collapse of slow start. This knowledge forms a critical component of your TCP expertise.