Loading content...
TCP Reno's Fast Recovery was a major leap forward, but it harbored a critical weakness: it couldn't handle multiple packet losses from a single window. When congestion was severe enough to drop two or more packets, Reno's performance would collapse, often performing worse than Tahoe.
The problem was subtle but devastating. Reno assumed that Fast Retransmit indicated a single loss. When the retransmitted packet was ACKed, Reno would exit Fast Recovery—only to discover that another packet was still missing. With no duplicate ACKs to trigger a new Fast Retransmit, the only recourse was a full retransmission timeout (RTO), throwing away all the benefits of Fast Recovery.
TCP NewReno, standardized in RFC 3782 (2004) and refined in RFC 6582 (2012), addresses this exact problem. Its key innovation is proper handling of partial ACKs—ACKs that acknowledge some but not all of the data outstanding when Fast Recovery began. Instead of prematurely exiting Fast Recovery, NewReno stays in recovery mode, retransmits the next suspected lost segment, and continues until all losses from the original window are recovered.
By the end of this page, you will understand how NewReno identifies and handles partial ACKs, the modified Fast Recovery algorithm that stays in recovery until complete, NewReno's performance advantages over Reno during congestion events, and the remaining inefficiencies that motivated SACK-based approaches.
The core of NewReno's improvement lies in how it interprets ACKs that arrive during Fast Recovery. To understand this, we need precise definitions:
Terminology:
High-water mark (recover): The highest sequence number sent when Fast Recovery was entered. This marks the boundary of data potentially affected by the initial congestion event.
Full ACK: An ACK that acknowledges the high-water mark or beyond. This indicates all losses from the original window have been repaired.
Partial ACK: An ACK that advances the cumulative acknowledgment (acknowledges new data) but does not reach the high-water mark. This indicates that the first lost segment was recovered, but another segment from the same window is still missing.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
/* * Partial ACK Detection in NewReno * * Scenario: Sender transmits 10 segments, [3] and [7] are lost * * Sender's view when entering Fast Recovery: * high_ack = 3 (last byte acked) * recover = 10 (highest byte sent) * * Step 1: 3 dupacks arrive for seq=3 -> Fast Retransmit [3] * Step 2: [3] arrives at receiver, gap filled from [3] * Step 3: Receiver sends ACK for seq=7 (cumulative: 3,4,5,6 complete) * BUT wait—[7] is also missing! * * Analysis of ACK for seq=7: * prev_ack = 3 * new_ack = 7 * recover = 10 * * new_ack > prev_ack? YES (acknowledges new data) * new_ack >= recover? NO (7 < 10) * * Conclusion: This is a PARTIAL ACK * Meaning: First loss recovered, but more losses exist */ typedef struct { uint32_t high_ack; // Highest ACK seen before FR uint32_t recover; // Highest SEQ sent when entering FR bool in_recovery; // Currently in Fast Recovery?} NewRenoState; typedef enum { NEW_ACK, // Normal ACK, not in recovery PARTIAL_ACK, // In recovery, advances but < recover FULL_ACK // In recovery, reaches or exceeds recover} AckType; AckType classify_ack(NewRenoState* s, uint32_t ack_num) { if (!s->in_recovery) { return NEW_ACK; } /* In Fast Recovery - check if partial or full */ if (ack_num >= s->recover) { return FULL_ACK; /* Recovery complete */ } else { return PARTIAL_ACK; /* More losses to repair */ }} /* * Visual: Partial ACK Timeline * * Segments: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] * X X * LOST LOST * * Time T0: 3 dupacks for [3] -> enter Fast Recovery * recover = 10, retransmit [3] * * Time T1: [3] arrives, receiver ACKs 7 (waiting for [7]) * ACK 7 is a PARTIAL ACK (7 < 10) * * Reno: Exit FR, later timeout on [7] <- BAD * NewReno: Stay in FR, retransmit [7] <- GOOD * * Time T2: [7] arrives, receiver ACKs 11 * ACK 11 is a FULL ACK (11 >= 10) * Exit Fast Recovery properly */Why Partial ACKs Are Crucial:
Partial ACKs carry critical information that Reno ignores:
Confirmed Loss Repair: The retransmitted segment was successfully received.
Another Loss Detected: The gap between the partial ACK and the recover point indicates at least one more lost segment.
Network Still Functional: Data is flowing in both directions. The path isn't completely blocked.
NewReno treats partial ACKs as signals to continue recovery rather than triggers to exit. This single semantic change transforms multiple-loss recovery from catastrophic (requiring timeout) to smooth (completed within a single Fast Recovery episode).
When a partial ACK arrives with sequence number N, NewReno can immediately retransmit segment N (the byte right after the ACK). By definition, if N < recover and the ACK just advanced past prior data, segment N must be missing. No guesswork required.
NewReno's Fast Recovery builds on Reno's foundation but adds specific logic for partial ACKs. The key changes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
/* TCP NewReno Complete Algorithm */ typedef struct { uint32_t cwnd; uint32_t ssthresh; uint32_t mss; uint32_t recover; // Highest seq sent at FR entry uint8_t dupacks; bool in_fast_recovery;} TCPNewReno; /* * Entry into Fast Recovery (same as Reno) */void newreno_enter_fast_recovery(TCPNewReno* s) { /* Mark recovery point */ s->recover = highest_seq_sent(); /* Set ssthresh to half of flight size */ uint32_t flight_size = get_bytes_in_flight(); s->ssthresh = MAX(flight_size / 2, 2 * s->mss); /* Retransmit first lost segment */ retransmit_first_unacked(); /* Set cwnd = ssthresh + 3*MSS (for 3 dupacks) */ s->cwnd = s->ssthresh + 3 * s->mss; s->in_fast_recovery = true;} /* * Handle ACK during Fast Recovery (NEWRENO LOGIC) */void newreno_on_ack_in_recovery(TCPNewReno* s, uint32_t ack_num) { if (ack_num >= s->recover) { /**************************** * FULL ACK - Recovery Complete ****************************/ /* Deflate cwnd to ssthresh */ s->cwnd = s->ssthresh; /* Exit Fast Recovery, resume CA */ s->in_fast_recovery = false; s->dupacks = 0; } else { /**************************** * PARTIAL ACK - Continue Recovery ****************************/ /* * Key difference from Reno: DON'T EXIT! * * The partial ACK tells us: * 1. First loss is repaired * 2. Another segment (starting at ack_num) is lost */ /* Step 1: Immediately retransmit next lost segment */ retransmit_segment_at(ack_num); /* * Step 2: Deflate cwnd by amount newly ACKed * * The inflated window included estimates for segments * that have now been ACKed. Remove them to maintain * appropriate window size. */ uint32_t bytes_acked = ack_num - last_ack(); s->cwnd -= bytes_acked; /* Add back 1 MSS (we just retransmitted one segment) */ s->cwnd += s->mss; /* * Step 3: Stay in Fast Recovery * Don't reset dupacks—we're still recovering */ /* * Step 4: Send new data if window permits * The deflated + inflated window may allow new transmissions */ if (can_send_new_data(s)) { send_new_segment(); } }} /* * Handle Duplicate ACKs */void newreno_on_dup_ack(TCPNewReno* s) { s->dupacks++; if (s->dupacks == 3 && !s->in_fast_recovery) { /* Enter Fast Recovery */ newreno_enter_fast_recovery(s); } else if (s->in_fast_recovery) { /* Already in FR: inflate window for self-clocking */ s->cwnd += s->mss; if (can_send_new_data(s)) { send_new_segment(); } }} /* * Timeout handling (same as Reno/Tahoe: full reset) */void newreno_on_timeout(TCPNewReno* s) { s->ssthresh = MAX(get_bytes_in_flight() / 2, 2 * s->mss); s->cwnd = s->mss; s->in_fast_recovery = false; s->dupacks = 0; go_back_n_retransmit();}Understanding Window Management:
NewReno's window management during partial ACKs is subtle but crucial:
Before Partial ACK: cwnd is inflated (ssthresh + N×MSS for N dup ACKs received).
On Partial ACK: The ACK acknowledges some data (let's say B bytes). We:
On Full ACK: cwnd deflates to ssthresh. Normal Congestion Avoidance resumes.
This careful accounting ensures the window never collapses prematurely but also doesn't grow unboundedly during extended recovery.
Immediately retransmitting on partial ACK is crucial. We know the segment at the ACK point is missing—waiting for 3 more dup ACKs would waste RTTs. NewReno optimistically retransmits, minimizing recovery time.
The state machines of Reno and NewReno are similar in structure but differ critically in how they transition out of Fast Recovery. This difference is the essence of NewReno's improvement.
| Event | TCP Reno | TCP NewReno |
|---|---|---|
| 3 Duplicate ACKs | Enter Fast Recovery Retransmit first loss | Enter Fast Recovery Retransmit first loss |
| Additional Dup ACK | cwnd += MSS (inflate) | cwnd += MSS (inflate) |
| Partial ACK | Exit FR, set cwnd=ssthresh No action for second loss! | Stay in FR Retransmit next loss Deflate cwnd appropriately |
| Full ACK | Exit FR, cwnd=ssthresh | Exit FR, cwnd=ssthresh |
| Timeout | Exit FR, reset to SS | Exit FR, reset to SS |
The Critical Difference:
The single-line change that distinguishes NewReno from Reno:
// Reno:
if (ack > last_ack) exit_fast_recovery();
// NewReno:
if (ack >= recover) exit_fast_recovery();
else handle_partial_ack();
This small change has enormous performance implications. When multiple packets are lost, Reno requires one timeout per additional loss (beyond the first). NewReno recovers all losses in approximately one RTT per loss, all within a single Fast Recovery episode.
Let's trace through a complete multiple-loss scenario to see exactly how NewReno handles it, contrasting with Reno's failure mode.
+==============================================================+| MULTIPLE LOSS SCENARIO WALKTHROUGH |+==============================================================+ Setup: Sender window = 10 segments, MSS = 1000 bytes Segments [3000, 4000) and [6000, 7000) are LOST Timeline:--------- T=0: Sender transmits segments 1-10 (bytes 0-10000) cwnd = 10000, ssthresh = 64000 T=0.5: Network congestion drops segments 3 and 6 T=1 RTT: Receiver sends ACK stream: ACK 1000 (got seg 1) ACK 2000 (got seg 2) ACK 3000 (got seg 3)... WAIT, seg 3 was lost! Actually: ACK 1000, ACK 2000, ACK 3000 Then receiver gets seg 4, but waiting for 3: DUP ACK 3000 (#1) Receiver gets seg 5: DUP ACK 3000 (#2) Receiver gets seg 7: <- Note: seg 6 also lost! DUP ACK 3000 (#3) --> FAST RETRANSMIT TRIGGER +----------------------------------------------------------+| At this point: 3 dup ACKs for byte 3000 || Segments 4,5,7,8,9,10 buffered at receiver (out-of-order) || Segments 3 and 6 are lost |+----------------------------------------------------------+ T=1.1 RTT: FAST RETRANSMIT triggered Sender state: recover = 10000 (highest sent) ssthresh = 5000 (cwnd/2) cwnd = 5000 + 3*1000 = 8000 Retransmit segment 3 (bytes 3000-4000) T=1.1-1.5 RTT: More dup ACKs arrive (for segs 8,9,10) Each one: cwnd += 1000 cwnd grows: 9000, 10000, 11000... New segments 11,12,... may be sent T=2 RTT: Retransmitted seg 3 arrives at receiver Receiver now has: 1,2,3,4,5,7,8,9,10 (missing 6) Receiver sends ACK 6000 (cumulative) PARTIAL ACK ANALYSIS: ack_num = 6000 recover = 10000 6000 < 10000 → PARTIAL ACK! +----------------------------------------------------------+| RENO vs NEWRENO DIVERGENCE POINT |+----------------------------------------------------------+ RENO BEHAVIOR: - Sees ACK 6000, thinks "recovery complete!" - Exits Fast Recovery - cwnd = ssthresh = 5000 - Later: no dup ACKs for seg 6 (seg 7+ already acked out-of-order) - RTO fires after several seconds - cwnd → 1000, ssthresh → 2500 - CATASTROPHIC PERFORMANCE NEWRENO BEHAVIOR: - Sees ACK 6000, checks: 6000 >= 10000? NO - This is a PARTIAL ACK → stay in Recovery - Immediately retransmit segment 6 (bytes 6000-7000) - Deflate: cwnd -= (6000-3000) = cwnd - 3000 - Inflate for retransmit: cwnd += 1000 - Net: cwnd ≈ 9000 (still substantial) - Reception of new data can continue T=3 RTT: Retransmitted seg 6 arrives Receiver now has all segments 1-10 Receiver sends ACK 10000 FULL ACK ANALYSIS: ack_num = 10000 recover = 10000 10000 >= 10000 → FULL ACK! NewReno exits Fast Recovery properly cwnd = ssthresh = 5000 Resume Congestion Avoidance +----------------------------------------------------------+| TOTAL RECOVERY TIME || Reno: ~2 RTT + RTO (often 1-3 seconds) || NewReno: ~3 RTT (typically 150-300ms) || || THROUGHPUT IMPACT || Reno: Massive pause during RTO, then Slow Start || NewReno: Continuous transmission with reduced window |+----------------------------------------------------------+For N losses in a window, NewReno requires approximately N RTTs for recovery (one per loss). Reno requires 1 RTT for the first loss plus (N-1) full RTOs for subsequent losses. Since RTO >> RTT, Reno's recovery time grows dramatically with loss count.
NewReno's performance advantage over Reno becomes most pronounced during congestion events that drop multiple packets. Let's quantify this improvement.
| Losses/Window | Reno Recovery | NewReno Recovery | Improvement |
|---|---|---|---|
| 1 | ~1 RTT (100ms) | ~1 RTT (100ms) | Equal |
| 2 | 1 RTT + RTO (~1.1s) | ~2 RTT (200ms) | 5.5× faster |
| 3 | 1 RTT + 2×RTO (~2.1s) | ~3 RTT (300ms) | 7× faster |
| 4 | 1 RTT + 3×RTO (~3.1s) | ~4 RTT (400ms) | 8× faster |
| 5 | 1 RTT + 4×RTO (~4.1s) | ~5 RTT (500ms) | 8× faster |
Throughput Impact Analysis:
Consider a long-running file transfer over a network with:
Expected throughput:
Reno:
NewReno:
Improvement: ~15-25% higher throughput under congestion
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
"""NewReno vs Reno Throughput Model Simplified analysis of steady-state throughput under loss."""import math def reno_recovery_time(losses_per_event, rtt, rto): """ Reno: First loss recovered in ~1 RTT, subsequent losses need RTO each """ if losses_per_event == 1: return rtt else: return rtt + (losses_per_event - 1) * rto def newreno_recovery_time(losses_per_event, rtt, rto): """ NewReno: Each loss recovered in ~1 RTT within single FR episode """ return losses_per_event * rtt def throughput_under_loss(recovery_time, rtt, loss_interval_packets, packet_size): """ Simplified throughput model: - Transmit loss_interval_packets at full rate - Pause for recovery_time - Repeat """ transmission_time = loss_interval_packets * packet_size * 8 / (100e6) # 100 Mbps cycle_time = transmission_time + recovery_time data_per_cycle = loss_interval_packets * packet_size return data_per_cycle / cycle_time # Bytes per second # ParametersRTT = 0.1 # 100msRTO = 1.0 # 1 secondLOSSES_PER_EVENT = 2LOSS_INTERVAL = 100 # packets between eventsPACKET_SIZE = 1500 # bytes reno_recovery = reno_recovery_time(LOSSES_PER_EVENT, RTT, RTO)newreno_recovery = newreno_recovery_time(LOSSES_PER_EVENT, RTT, RTO) reno_throughput = throughput_under_loss(reno_recovery, RTT, LOSS_INTERVAL, PACKET_SIZE)newreno_throughput = throughput_under_loss(newreno_recovery, RTT, LOSS_INTERVAL, PACKET_SIZE) print(f"Reno recovery time: {reno_recovery*1000:.0f} ms")print(f"NewReno recovery time: {newreno_recovery*1000:.0f} ms")print(f"")print(f"Reno throughput: {reno_throughput/1e6:.2f} MB/s")print(f"NewReno throughput: {newreno_throughput/1e6:.2f} MB/s")print(f"Improvement: {(newreno_throughput/reno_throughput - 1)*100:.1f}%") # Output:# Reno recovery time: 1100 ms# NewReno recovery time: 200 ms# # Reno throughput: 10.05 MB/s# NewReno throughput: 11.03 MB/s# Improvement: 9.7%For single-packet losses, NewReno and Reno behave identically. The partial ACK mechanism only activates when multiple losses occur. In lightly congested networks with infrequent, isolated losses, the two algorithms perform the same.
Implementing NewReno requires careful attention to several subtle details. The RFC 6582 specification addresses edge cases that early implementations handled incorrectly.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
/* NewReno Edge Cases and Refinements */ /* * Edge Case 1: Retransmission from Slow Start * * If Fast Retransmit triggers while in Slow Start (cwnd < ssthresh), * normal entry applies. However, the initial cwnd may be small, * leading to limited recovery parallelism. */void enter_fr_from_slow_start(TCPNewReno* s) { /* ssthresh = max(cwnd/2, 2*MSS) - may be very small */ s->ssthresh = max(s->cwnd / 2, 2 * s->mss); /* cwnd = ssthresh + 3*MSS - ensures at least 5 MSS worth */ s->cwnd = s->ssthresh + 3 * s->mss; /* Recovery proceeds normally */} /* * Edge Case 2: Buggy Implementations Sending Wrong ACKs * * Some receivers incorrectly re-ACK the same sequence on receiving * out-of-order data, rather than sending true duplicates. * This creates phantom "partial ACKs" that don't indicate real loss. * * Mitigation: Track that ACKs actually advance, don't just differ. */bool is_real_partial_ack(uint32_t new_ack, uint32_t last_ack, uint32_t recover) { return (new_ack > last_ack) && /* Must advance */ (new_ack < recover); /* Less than recover */} /* * Edge Case 3: Reordering vs. Loss * * If packets arrive out of order (not lost), we might enter * Fast Recovery unnecessarily. RFC 6582 recommends: * - Don't enter FR if last ACK was recent and we haven't sent much * - Consider DSACK or SACK for better loss identification */ /* * Edge Case 4: Sequence Number Wraparound * * TCP sequence numbers are 32-bit and wrap around. * Comparisons like "ack_num >= recover" must use serial arithmetic * (RFC 1982) to correctly handle numbers near UINT32_MAX. */bool seq_gte(uint32_t a, uint32_t b) { /* Serial number arithmetic comparison */ return (int32_t)(a - b) >= 0;} /* * Edge Case 5: Multiple Fast Retransmits * * If another set of 3 dupACKs arrives while already in FR * (e.g., from new data sent during recovery being lost), * NewReno should NOT re-enter FR. The 'recover' point only * updates when entering FR, not during. */void on_dup_ack(TCPNewReno* s) { s->dupacks++; if (s->dupacks >= 3) { if (!s->in_fast_recovery) { /* First time: enter FR */ newreno_enter_fast_recovery(s); } else { /* Already in FR: just inflate, don't re-enter */ s->cwnd += s->mss; } }}While NewReno significantly improves upon Reno, it still has fundamental limitations that motivated the development of more advanced mechanisms like Selective Acknowledgment (SACK).
The Fundamental Problem: Information Asymmetry
The receiver knows exactly which segments arrived (and thus, by inference, which are missing). But cumulative ACKs can only convey one piece of information: the byte stream is contiguous up to sequence number N.
This information poverty forces NewReno to guess and wait:
Each cycle takes one RTT. There's no way to parallelize.
Selective ACKs (SACK) solve this information problem by allowing receivers to report exactly which segments they've received, not just the contiguous prefix. With SACK, a sender can learn about multiple losses in one RTT and retransmit them all immediately—reducing N-RTT recovery to approximately 1 RTT recovery regardless of loss count.
| Losses/Window | NewReno Recovery | SACK Recovery |
|---|---|---|
| 1 | ~1 RTT | ~1 RTT |
| 2 | ~2 RTT | ~1 RTT |
| 5 | ~5 RTT | ~1-2 RTT |
| 10 | ~10 RTT | ~1-2 RTT |
| 50 | ~50 RTT | ~2-3 RTT |
Despite its limitations, NewReno remains relevant today as a baseline and fallback. Its simplicity, universal support, and reasonable performance make it a robust choice when more advanced mechanisms aren't available.
Current Role:
Standard Status:
The Relationship with CUBIC and BBR:
Modern variants like CUBIC and BBR modify the congestion avoidance and window growth behaviors but typically retain NewReno-style Fast Retransmit and Fast Recovery. When loss occurs, CUBIC and BBR often fall back to NewReno (or SACK-enhanced) recovery mechanics.
In practice, most modern TCP connections negotiate SACK during the three-way handshake. NewReno's partial ACK handling becomes relevant only when SACK is unavailable. However, understanding NewReno is essential for understanding SACK's improvements—and for diagnosing performance issues when SACK negotiation fails.
TCP NewReno represents the final refinement of the Reno lineage, fixing the critical multiple-loss weakness while maintaining simplicity. Let's consolidate the essential concepts:
What's Next:
In the next page, we explore TCP CUBIC, the dominant congestion control algorithm in today's Internet. CUBIC takes a radically different approach to window growth, replacing the linear Congestion Avoidance growth with a cubic function that provides faster recovery after loss while remaining fair across different RTT paths.
You now have comprehensive understanding of TCP NewReno—the partial ACK mechanism, modified Fast Recovery behavior, performance improvements over Reno, and the fundamental limitations that motivated SACK. You can trace through multiple-loss scenarios and explain why NewReno recovers without timeout while Reno fails.