Loading learning content...
We've established that hidden terminals cause collisions because stations cannot sense each other's transmissions. The solution requires sharing receiver-side information with all potentially interfering stations. This is precisely what the RTS/CTS (Request-to-Send/Clear-to-Send) mechanism accomplishes.
First proposed by Phil Karn in his MACA protocol (1990) and refined in MACAW (1994), RTS/CTS became part of the original IEEE 802.11 standard in 1997. It remains the primary hidden terminal mitigation mechanism in modern Wi-Fi networks.
Core Insight: Instead of transmitting data immediately (which hidden terminals might corrupt), first perform a short handshake that 'announces' the upcoming transmission to all stations within range of either the sender OR the receiver.
RTS/CTS extends carrier sensing from local (physical) to neighborhood-wide (virtual). The sender's RTS reaches all stations near the sender; the receiver's CTS reaches all stations near the receiver. Together, they notify everyone who might cause or suffer a collision to defer. This 'virtual' sensing uses NAV timers rather than physical signal detection.
Why 'Virtual Carrier Sensing'?
Physical carrier sensing detects RF energy on the channel. Virtual carrier sensing uses information within control frames to determine channel busy periods. The NAV (Network Allocation Vector) is a countdown timer that stations set based on Duration fields in received frames. When NAV > 0, the station considers the channel busy regardless of physical sensing results.
This page provides a comprehensive analysis of RTS/CTS:
The RTS/CTS handshake extends the basic data/ACK exchange with two additional control frames. Let's trace through the complete sequence when Station A sends data to Station B (an Access Point).
Step 0: Contention and Backoff
Before sending RTS, Station A must win channel access:
Step 1: RTS Transmission (A → B)
Station A transmits a short RTS frame:
Step 2: SIFS Wait
Station B waits for SIFS (Short Interframe Space) = 10-16 μs depending on PHY.
Step 3: CTS Response (B → A)
Station B transmits CTS:
Step 4: SIFS Wait
Station A waits SIFS after receiving CTS.
Step 5: Data Frame Transmission (A → B)
Station A transmits the actual data frame:
Step 6: SIFS Wait
Station B waits SIFS after receiving data frame.
Step 7: ACK Response (B → A)
Station B sends acknowledgment:
Step 8: Completion
The key insight: Hidden terminals couldn't hear Station A's RTS (they're hidden from A). But they CAN hear Station B's CTS, because they're within range of B (that's what makes them a collision threat—they can interfere at B). By hearing the CTS, hidden terminals learn 'Station B is about to receive' and defer via NAV.
RTS Collision: If two stations send RTS simultaneously (both hidden from each other but reaching B), collision occurs at B. Neither receives CTS. Both timeout and enter exponential backoff with increased contention window.
CTS Not Received: If Station A sends RTS but doesn't receive CTS:
Data/ACK Failures: If data or ACK is lost despite successful RTS/CTS:
Understanding the exact format of RTS and CTS frames is essential for protocol comprehension and exam preparation.
The Request-to-Send frame is a control frame (Type: 01, Subtype: 1011):
| Field | Size | Description |
|---|---|---|
| Frame Control | 2 bytes | Type=01 (Control), Subtype=1011 (RTS) |
| Duration | 2 bytes | Time (μs) until end of ACK |
| Receiver Address (RA) | 6 bytes | Intended receiver (destination) |
| Transmitter Address (TA) | 6 bytes | Frame sender (source) |
| FCS | 4 bytes | Frame Check Sequence (CRC-32) |
Duration Field Calculation:
Duration_RTS = SIFS + CTS_time + SIFS + Data_time + SIFS + ACK_time
All times in microseconds. This tells all receivers how long they should defer.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
"""RTS/CTS Duration Field Calculation802.11 frame timing analysis""" def calculate_frame_time_ofdm(frame_bytes: int, data_rate_mbps: float, symbol_time_us: float = 4.0, preamble_us: float = 20.0) -> float: """ Calculate transmission time for OFDM-based PHY (802.11a/g/n/ac). Parameters: - frame_bytes: Total frame size in bytes including MAC header and FCS - data_rate_mbps: PHY data rate in Mbps - symbol_time_us: OFDM symbol duration (4 μs for 20 MHz channel) - preamble_us: PHY preamble and header duration (20 μs for 802.11a/g) Returns: Transmission time in microseconds """ # Service field (16 bits) + MAC frame + tail (6 bits) total_bits = 16 + (frame_bytes * 8) + 6 # Bits per symbol at given data rate bits_per_symbol = data_rate_mbps * symbol_time_us # Number of OFDM symbols (ceiling) import math n_symbols = math.ceil(total_bits / bits_per_symbol) # Total time = preamble + (symbols × symbol_time) return preamble_us + (n_symbols * symbol_time_us) def calculate_rts_cts_duration( data_frame_bytes: int, data_rate_mbps: float, control_rate_mbps: float = 6.0, # Typically 6 Mbps for control frames sifs_us: float = 16.0 # SIFS for 802.11g) -> dict: """ Calculate all Duration field values for RTS/CTS exchange. 802.11g example with OFDM. """ # Frame sizes RTS_BYTES = 20 CTS_BYTES = 14 ACK_BYTES = 14 MAC_HEADER_BYTES = 34 # Typical MAC header for data total_data_bytes = MAC_HEADER_BYTES + data_frame_bytes + 4 # +4 for FCS # Calculate individual transmission times rts_time = calculate_frame_time_ofdm(RTS_BYTES, control_rate_mbps) cts_time = calculate_frame_time_ofdm(CTS_BYTES, control_rate_mbps) data_time = calculate_frame_time_ofdm(total_data_bytes, data_rate_mbps) ack_time = calculate_frame_time_ofdm(ACK_BYTES, control_rate_mbps) # Duration field values duration_rts = sifs_us + cts_time + sifs_us + data_time + sifs_us + ack_time duration_cts = duration_rts - cts_time - sifs_us duration_data = sifs_us + ack_time return { 'rts_time': rts_time, 'cts_time': cts_time, 'data_time': data_time, 'ack_time': ack_time, 'sifs': sifs_us, 'duration_rts': duration_rts, 'duration_cts': duration_cts, 'duration_data': duration_data, 'total_exchange_time': rts_time + sifs_us + cts_time + sifs_us + data_time + sifs_us + ack_time } # Example: 1500-byte payload at 54 Mbps data rateresult = calculate_rts_cts_duration( data_frame_bytes=1500, data_rate_mbps=54.0, control_rate_mbps=6.0, sifs_us=16.0) print("=== RTS/CTS Duration Calculation ===")print(f"Data payload: 1500 bytes at 54 Mbps")print(f"Control frames at: 6 Mbps")print()print("--- Frame Transmission Times ---")print(f"RTS (20 bytes): {result['rts_time']:.1f} μs")print(f"CTS (14 bytes): {result['cts_time']:.1f} μs")print(f"Data (~1540 bytes): {result['data_time']:.1f} μs")print(f"ACK (14 bytes): {result['ack_time']:.1f} μs")print(f"SIFS: {result['sifs']:.1f} μs")print()print("--- Duration Field Values ---")print(f"Duration in RTS: {result['duration_rts']:.0f} μs")print(f"Duration in CTS: {result['duration_cts']:.0f} μs")print(f"Duration in Data: {result['duration_data']:.0f} μs")print()print(f"Total exchange time: {result['total_exchange_time']:.0f} μs")The Clear-to-Send frame is smaller than RTS:
| Field | Size | Description |
|---|---|---|
| Frame Control | 2 bytes | Type=01 (Control), Subtype=1100 (CTS) |
| Duration | 2 bytes | Time (μs) until end of ACK |
| Receiver Address (RA) | 6 bytes | Copied from TA of RTS (the sender) |
| FCS | 4 bytes | Frame Check Sequence (CRC-32) |
Note: CTS has no Transmitter Address field. It's implied to be from whoever received the RTS (the destination in the RTS).
Duration Field Calculation:
Duration_CTS = Duration_RTS - SIFS - CTS_time
This creates a decreasing chain of Duration values through the exchange.
Each subsequent frame's Duration reflects the remaining time until ACK completion. Stations may receive only some frames (e.g., CTS but not RTS). The decreasing Duration ensures they set appropriate NAV regardless of which frame they captured. This is 'belt and suspenders' reliability.
| Frame Type | Type Value | Subtype Value | Binary Pattern |
|---|---|---|---|
| RTS | 01 (Control) | 1011 | 10110100 |
| CTS | 01 (Control) | 1100 | 11000100 |
| ACK | 01 (Control) | 1101 | 11010100 |
| PS-Poll | 01 (Control) | 1010 | 10100100 |
RTS/CTS is not free—it adds overhead to every frame exchange. Understanding this overhead is crucial for determining when to enable it.
Time Overhead per Data Frame:
Overhead_time = RTS_time + SIFS + CTS_time + SIFS
For 802.11g at 6 Mbps control rate:
Byte Overhead:
Overhead_bytes = 20 (RTS) + 14 (CTS) = 34 bytes
At air time:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
"""RTS/CTS Efficiency and Break-even AnalysisWhen does RTS/CTS overhead pay off?""" import math def ofdm_frame_time(bytes_count: int, rate_mbps: float, preamble_us: float = 20.0) -> float: """Calculate OFDM frame transmission time in μs.""" bits = 16 + (bytes_count * 8) + 6 # Service + data + tail bits_per_symbol = rate_mbps * 4 # 4 μs symbol time symbols = math.ceil(bits / bits_per_symbol) return preamble_us + (symbols * 4) def calculate_efficiency(payload_bytes: int, data_rate_mbps: float, control_rate_mbps: float = 6.0, sifs_us: float = 16.0, difs_us: float = 34.0, use_rts_cts: bool = False) -> dict: """ Calculate throughput efficiency with and without RTS/CTS. """ # Frame sizes mac_header = 34 fcs = 4 total_data_bytes = mac_header + payload_bytes + fcs # Transmission times data_time = ofdm_frame_time(total_data_bytes, data_rate_mbps) ack_time = ofdm_frame_time(14, control_rate_mbps) if use_rts_cts: rts_time = ofdm_frame_time(20, control_rate_mbps) cts_time = ofdm_frame_time(14, control_rate_mbps) total_time = (difs_us + rts_time + sifs_us + cts_time + sifs_us + data_time + sifs_us + ack_time) else: rts_time = 0 cts_time = 0 total_time = difs_us + data_time + sifs_us + ack_time # Efficiency = payload bits / total channel time payload_bits = payload_bytes * 8 efficiency = payload_bits / total_time # Mbps return { 'payload_bytes': payload_bytes, 'data_rate_mbps': data_rate_mbps, 'rts_time': rts_time, 'cts_time': cts_time, 'data_time': data_time, 'ack_time': ack_time, 'total_time': total_time, 'efficiency_mbps': efficiency, 'efficiency_percent': (efficiency / data_rate_mbps) * 100, 'overhead_time': rts_time + sifs_us + cts_time + sifs_us if use_rts_cts else 0 } def find_breakeven_frame_size(collision_rate: float, data_rate_mbps: float = 54.0): """ Find frame size where RTS/CTS overhead equals collision cost savings. Collision cost = P(collision) × (data_frame_time + retry_overhead) RTS/CTS benefit = collision on short RTS instead of long data Simplified model: RTS/CTS beneficial when collision rate is high enough that savings from avoiding data frame collisions exceed RTS/CTS overhead. """ control_rate = 6.0 # Mbps sifs = 16.0 # μs rts_time = ofdm_frame_time(20, control_rate) cts_time = ofdm_frame_time(14, control_rate) rts_cts_overhead = rts_time + sifs + cts_time + sifs # ~128 μs # Break-even: collision_rate × data_time_saved = rts_cts_overhead # data_time_saved ≈ data_time (if collision, retransmit whole frame) # Break-even data_time = rts_cts_overhead / collision_rate if collision_rate <= 0: return float('inf') breakeven_data_time = rts_cts_overhead / collision_rate # Convert back to frame size (approximate) # For 54 Mbps: ~250 ns per byte (rough) # More accurate: solve ofdm_frame_time equation for size in range(100, 3000, 10): if ofdm_frame_time(size + 38, data_rate_mbps) >= breakeven_data_time: return size return 3000 print("=== RTS/CTS Efficiency Analysis ===")print() # Compare efficiency at different frame sizesprint("--- Efficiency vs Frame Size (54 Mbps data rate) ---")print(f"{'Payload':>8} | {'Without RTS/CTS':>18} | {'With RTS/CTS':>16} | {'Overhead':>10}")print("-" * 62) for payload in [64, 256, 512, 1024, 1500, 2000]: without = calculate_efficiency(payload, 54.0, use_rts_cts=False) with_rts = calculate_efficiency(payload, 54.0, use_rts_cts=True) print(f"{payload:>6} B | {without['efficiency_mbps']:>8.2f} Mbps ({without['efficiency_percent']:>4.1f}%) | " f"{with_rts['efficiency_mbps']:>6.2f} Mbps ({with_rts['efficiency_percent']:>4.1f}%) | " f"{with_rts['overhead_time']:>6.0f} μs") print()print("--- Break-even Frame Size by Collision Rate ---")print(f"{'Collision Rate':>15} | {'Break-even Size':>16}")print("-" * 35)for rate in [0.01, 0.05, 0.10, 0.20, 0.30, 0.50]: be_size = find_breakeven_frame_size(rate) print(f"{rate:>14.0%} | {be_size:>14} bytes") print()print("Insight: RTS/CTS beneficial when collision rate × data size > overhead")RTS/CTS is beneficial when the cost of potential collisions exceeds the overhead cost.
Cost of Collision (without RTS/CTS):
Cost of RTS Collision (with RTS/CTS):
Break-Even Equation:
RTS/CTS_overhead = P_collision × Data_frame_time_saved
If collision probability P = 10%:
If collision probability P = 5%:
802.11 default RTS threshold is often 2347 bytes (effectively disabled since max frame is 2346 bytes). For hidden terminal environments, lower to 500-1000 bytes. Too low wastes overhead on small frames; too high risks expensive data collisions. Monitor retry rates to tune.
| Frame Size | Efficiency Without RTS/CTS | Efficiency With RTS/CTS | Overhead Penalty |
|---|---|---|---|
| 64 bytes | ~15 Mbps (28%) | ~3 Mbps (6%) | -78% (very bad) |
| 256 bytes | ~28 Mbps (52%) | ~11 Mbps (20%) | -61% (bad) |
| 512 bytes | ~35 Mbps (65%) | ~18 Mbps (33%) | -49% (significant) |
| 1024 bytes | ~41 Mbps (76%) | ~26 Mbps (48%) | -37% (moderate) |
| 1500 bytes | ~44 Mbps (81%) | ~30 Mbps (56%) | -31% (acceptable) |
| 2000 bytes | ~46 Mbps (85%) | ~33 Mbps (61%) | -28% (acceptable) |
The Network Allocation Vector (NAV) is the implementation mechanism for virtual carrier sensing. Understanding NAV operation is essential for comprehending how RTS/CTS actually silences potential interferers.
What NAV Is:
NAV Update Rules:
Only update if new value is larger:
NAV counts down continuously:
NAV applies to channel access:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
"""NAV (Network Allocation Vector) State MachineConceptual implementation showing NAV operation""" from dataclasses import dataclassfrom enum import Enum, autoimport time class ChannelState(Enum): IDLE = auto() PHYSICAL_BUSY = auto() VIRTUAL_BUSY = auto() BOTH_BUSY = auto() @dataclassclass StationNAV: """Represents a station's NAV state.""" nav_value: float = 0 # Current NAV in microseconds nav_set_time: float = 0 # When NAV was last set def update_nav(self, duration_us: float, current_time: float) -> bool: """ Update NAV based on received Duration field. Returns True if NAV was updated, False if ignored. 802.11 Rule: Only update if new duration > remaining NAV """ remaining_nav = self.get_remaining_nav(current_time) if duration_us > remaining_nav: self.nav_value = duration_us self.nav_set_time = current_time return True return False def get_remaining_nav(self, current_time: float) -> float: """Get remaining NAV time in microseconds.""" elapsed = (current_time - self.nav_set_time) * 1e6 # Convert to μs remaining = self.nav_value - elapsed return max(0, remaining) def is_nav_zero(self, current_time: float) -> bool: """Check if NAV has expired.""" return self.get_remaining_nav(current_time) <= 0 class Station: """Wireless station with physical and virtual carrier sensing.""" def __init__(self, station_id: str): self.id = station_id self.nav = StationNAV() self.physical_rx_active = False # Physical carrier sense def receive_frame(self, frame_type: str, duration_us: float, from_station: str, current_time: float): """Process received frame and update NAV if applicable.""" print(f"[{self.id}] Received {frame_type} from {from_station}, " f"Duration={duration_us} μs") if frame_type in ['RTS', 'CTS', 'DATA']: updated = self.nav.update_nav(duration_us, current_time) if updated: print(f" → NAV updated to {duration_us} μs") else: remaining = self.nav.get_remaining_nav(current_time) print(f" → NAV not updated (remaining {remaining:.0f} μs > {duration_us} μs)") def get_channel_state(self, current_time: float) -> ChannelState: """Determine channel state from physical and virtual sensing.""" phy_busy = self.physical_rx_active virt_busy = not self.nav.is_nav_zero(current_time) if phy_busy and virt_busy: return ChannelState.BOTH_BUSY elif phy_busy: return ChannelState.PHYSICAL_BUSY elif virt_busy: return ChannelState.VIRTUAL_BUSY else: return ChannelState.IDLE def can_transmit(self, current_time: float) -> bool: """Check if station is allowed to transmit.""" state = self.get_channel_state(current_time) if state == ChannelState.IDLE: return True print(f" [{self.id}] Cannot transmit - channel state: {state.name}") return False # Simulation exampledef simulate_rts_cts_nav_operation(): """ Simulate NAV operation during RTS/CTS exchange. Scenario: - Station A wants to send to AP - Station B is in range of A (hears RTS) - Station C is hidden from A but in range of AP (hears CTS only) """ print("=== NAV Operation Simulation ===") print() # Create stations station_a = Station("A (Sender)") station_b = Station("B (Visible)") station_c = Station("C (Hidden)") ap = Station("AP (Receiver)") # Timing (in seconds for simulation, values in μs for NAV) t = 0.0 # RTS/CTS timing for 1500-byte frame at 54 Mbps SIFS = 16 RTS_DURATION = 1000 # μs until end of ACK CTS_DURATION = RTS_DURATION - 44 - SIFS # After CTS transmission DATA_DURATION = 100 # μs (ACK time only) print(f"T={t:.6f}s: Station A sends RTS") # Station B hears RTS, sets NAV station_b.receive_frame("RTS", RTS_DURATION, "A", t) # Station C does NOT hear RTS (hidden from A) print(f" Station C: Does not receive RTS (hidden from A)") print() t += 0.000060 # 60 μs for RTS + SIFS print(f"T={t:.6f}s: AP sends CTS") # Station B hears CTS (updates NAV with lower value - no change) station_b.receive_frame("CTS", CTS_DURATION, "AP", t) # Station C hears CTS (first NAV update!) station_c.receive_frame("CTS", CTS_DURATION, "AP", t) print() t += 0.000050 # 50 μs for CTS + SIFS print(f"T={t:.6f}s: Station A sends DATA") print(f" Station B: Remaining NAV = {station_b.nav.get_remaining_nav(t):.0f} μs") print(f" Station C: Remaining NAV = {station_c.nav.get_remaining_nav(t):.0f} μs") # Can stations B or C transmit? print() print("--- Can stations transmit during data? ---") station_b.can_transmit(t) station_c.can_transmit(t) # After exchange completes t += 0.001100 # 1100 μs for data + ACK print() print(f"T={t:.6f}s: Exchange complete") print(f" Station B: Remaining NAV = {station_b.nav.get_remaining_nav(t):.0f} μs") print(f" Station C: Remaining NAV = {station_c.nav.get_remaining_nav(t):.0f} μs") print(f" Station B can transmit: {station_b.can_transmit(t)}") print(f" Station C can transmit: {station_c.can_transmit(t)}") simulate_rts_cts_nav_operation()Physical carrier sensing (CCA - Clear Channel Assessment) detects RF energy. Virtual carrier sensing (NAV) tracks announced reservations. A station considers the channel busy if EITHER indicates busy. This dual mechanism ensures both physical interference and protocol reservations are respected.
The Duration field appears in most 802.11 frames, not just RTS/CTS:
Data Frames:
Management Frames:
PS-Poll (Power Save):
CF-End (Contention Free End):
Deploying RTS/CTS effectively requires understanding its configuration options and how to diagnose related issues.
1. RTS Threshold (dot11RTSThreshold)
Recommended Settings:
2. CTS-to-Self
An alternative protection mechanism:
Symptom: High RTS retry rate but low data retry rate
Symptom: High RTS retry rate AND high data retry rate
Symptom: Low throughput with RTS/CTS enabled, normal without
Symptom: NAV-related delays visible in protocol analysis
Use SNMP or vendor-specific monitoring to track: dot11RTSSuccessCount (RTS sent and CTS received), dot11RTSFailureCount (RTS sent, no CTS), dot11TransmittedFrameCount, dot11RetryCount. If RTS success rate is high but retry count remains high, hidden terminals may extend beyond CTS range.
| Observation | Likely Cause | Recommended Action |
|---|---|---|
| Throughput drops when clients increase | Hidden terminal collisions | Enable RTS/CTS (lower threshold) |
| Throughput drops when RTS enabled | Overhead exceeds benefit | Raise RTS threshold or disable |
| Specific client pair has issues | Local hidden terminal relationship | Reposition clients or AP |
| Good uplink, poor downlink | Not hidden terminal issue | Check AP queue/scheduling |
| Poor uplink, good downlink | Hidden terminals on uplink | Enable RTS/CTS for uplink |
| RTS timeout common | RTS collision or AP not responding | Check AP receiver health; channel interference |
CTS-to-Self is a simplified protection mechanism that reduces overhead compared to full RTS/CTS while still providing some hidden terminal protection.
Instead of:
CTS-to-Self does:
Protection Coverage:
Primary Use Case: Mixed-Mode Protection in 802.11g/n
When an 802.11g/n network has legacy 802.11b clients:
802.11g/n networks have protection mode settings:
Protection Disabled:
CTS-to-Self Protection:
RTS/CTS Protection:
Configuration Options (vendor-specific names):
802.11n 'Greenfield' mode disables all legacy protection for maximum efficiency. However, if ANY legacy (802.11a/b/g) device exists nearby, Greenfield causes severe interference. Use only in isolated environments with no legacy devices possible.
802.11ax (Wi-Fi 6) introduces significant enhancements that complement and in some cases supersede traditional RTS/CTS for hidden terminal mitigation.
Multi-User RTS (MU-RTS):
Trigger Frames:
How BSS Color Works:
Spatial Reuse Implications:
Hidden Terminal Consideration:
TWT Concept:
Hidden Terminal Benefit:
Trade-off:
For maximum hidden terminal protection with 802.11ax: (1) Enable OFDMA scheduling for dense environments, (2) Configure BSS Color properly across adjacent APs, (3) Use MU-RTS for multi-user downlink, and (4) Keep traditional RTS/CTS enabled for legacy protection and unicast traffic. Wi-Fi 6 reduces but doesn't eliminate hidden terminals.
We've comprehensively examined the RTS/CTS mechanism—its operation, overhead, configuration, and evolution. Let's consolidate the essential knowledge:
The next page provides an in-depth exploration of the NAV (Network Allocation Vector)—examining its implementation details, update rules, reset mechanisms, and interaction with physical carrier sensing. Understanding NAV completes your mastery of virtual carrier sensing.
You now possess comprehensive knowledge of the RTS/CTS mechanism: from frame formats and timing to overhead calculations and practical configuration. This understanding is essential for both 802.11 network optimization and GATE/competitive exam preparation.