macOS · Wi-Fi · measurement

Eight decibels, and four settings that measured as noise

Reading a weak Wi-Fi link from the terminal on macOS: what signal, noise and the MCS index describe, the two router settings that fixed a real fault, how the RTS handshake works, and why the remaining changes proved nothing.

Nine bars of wildly different heights under one calm horizontal average line, with the shortest bar picked out in red.
Four samples told one story. Twelve told a different one.

A Wi-Fi link that felt slow turned out to be genuinely misconfigured, and two router settings recovered eight decibels of margin. The tuning that followed produced four more changes, confident explanations for each, and no measurable effect whatsoever. The Wi-Fi diagnosis is useful on its own. The reason the second half failed is the more transferable part.

The short versionTwo settings mattered: the router was transmitting at Middle power, and it sat on the most crowded channel in the band. Correcting both moved the link from -80 dBm to -75 dBm and from the second-slowest modulation rung to a usable one. Every change after that produced differences smaller than the run-to-run variance, which had not yet been measured.

Reading the link from the terminal

macOS has removed the old airport command-line tool, and wdutil info returns little without root. The remaining source needs no privileges:

system_profiler SPAirPortDataType \
  | sed -n '/Current Network Information/,/Other Local/p'

On the link in question that returned:

PHY Mode: 802.11ax
Channel: 36 (5GHz, 80MHz)
Signal / Noise: -81 dBm / -95 dBm
Transmit Rate: 136
MCS Index: 1

Four of those five lines carry diagnostic weight.

Signal at -81 dBm is weak. The scale is logarithmic and negative, so closer to zero is stronger. Around -50 dBm is excellent, -67 dBm is the usual floor for reliable video, and -80 dBm sits at the edge of the usable range.

Noise at -95 dBm is clean, close to the thermal floor, meaning the air itself is quiet. The distinction matters: a weak signal in quiet air is a distance problem, while a strong signal in noisy air is an interference problem. The two have different remedies, and this was plainly the former.

The gap between the two is what the radio responds to. Subtracting signal from noise gives the signal-to-noise ratio, 14 dB here. That single figure sets the ceiling on link speed, and it is the number worth optimising.

802.11ax on an 80 MHz channel should be fast. That combination is capable of well over a gigabit. Delivering 136 Mbps indicates something is badly wrong, and the MCS index identifies what.

What the MCS index describes

MCS stands for Modulation and Coding Scheme. It is the gear the two radios have negotiated, selected automatically from link quality. Two properties combine in it: how many bits ride on each radio symbol, and how much of the transmission is spent on error correction rather than payload.

802.11ax defines MCS 0 through MCS 11. The lower rungs:

indexmodulationbits per symbolroughly needs
0BPSK15 dB
1QPSK28 dB
316-QAM414 dB
764-QAM625 dB
111024-QAM1037 dB

MCS 1 is therefore the second-slowest rung of eleven: two bits per symbol, with half the transmission given over to error correction. It is what a link falls back to when it is barely holding together. On 802.11ax hardware that is a car on a motorway stuck in second gear.

This makes the MCS index the most informative figure on the screen. Signal strength describes the conditions; the MCS index reveals what the radio decided to do about them.

Scanning the band

The same command lists every network in range, which shows whether airtime is contended:

system_profiler SPAirPortDataType \
  | grep -E 'Channel: [0-9]+ \(5GHz' | sort | uniq -c | sort -rn
   3 Channel: 36 (5GHz, 80MHz)
   2 Channel: 40 (5GHz, 80MHz)

This is where an 80 MHz channel stops being an unambiguous benefit. A channel anchored at 36 with 80 MHz of width occupies 36, 40, 44 and 48 together. Every network listed was running 80 MHz, so the whole lower band formed a single contended block in which the anchor number separated nobody:

  channel:     36      40      44      48
               +-------+-------+-------+-------+
  local   ch36 |###############################|  80 MHz
  neighbour A  |###############################|  80 MHz, -67 dBm
  neighbour B  |###############################|  80 MHz, -71 dBm
               +-------+-------+-------+-------+
               |<--------- all overlap -------->|

One neighbour arrived at -67 dBm, thirteen decibels stronger than the local access point. The link was sharing air with a signal far louder than the one it wanted.

A graphical scanner earns its place here. The terminal reports which channels are occupied; a spectrum view shows the shape of that occupation, and in this case revealed that everything above channel 60 was completely empty.

The setting that was actually wrong

The empty upper band looked like the answer and proved unavailable. The router offered channels 36, 40, 44 and 48 and nothing else, because TP-Link locks the radar-detection channels out of this model's European firmware. The asymmetry is worth noting before planning around those channels: the Mac supported all 48 of them, and the access point exposed four.

The settings page did reveal something better. Transmit Power was set to Middle.

That is the entire diagnosis. A link at the edge of usable range, in quiet air, with the transmitter deliberately throttled. Raising it to High and moving the anchor from 36 to 44 produced:

metricbeforeafter
signal-80 dBm-75 dBm
noise-95 dBm-97 dBm
signal-to-noise14 dB22 dB
MCS index13 to 4

Eight decibels of margin and two rungs up the ladder, from two dropdowns. This part of the session holds up under repetition.

Channel width, and a conclusion drawn too early

With real ground gained, the tuning continued. The theory was sound: at low signal-to-noise a narrower channel concentrates the same transmit power into less bandwidth, worth roughly 3 dB, and a link that cannot sustain high modulation gains nothing from extra width. The channel was narrowed from 80 MHz to 40.

The first measurement agreed convincingly. 191 Mbps against 151 before, with the quality metrics agreeing too: better signal, lower noise floor, MCS 3 and 4 instead of 1 and 2. A coherent result with a mechanism behind it.

Widening back to 80 MHz to confirm returned 190 and 191 Mbps. Identical. Narrowing again to check returned 118, 136, 137 and 146.

At that point the explanation collapsed, because one setting had produced 191 Mbps once and an average of 134 Mbps a few minutes later. The comparisons had been single samples of a quantity that moves on its own.

Establishing the noise band first

The remedy is unglamorous: measure the unchanged system repeatedly and find out how much it varies before attributing anything to a change. Four runs, twenty seconds apart, recording link state alongside each:

for i in 1 2 3 4; do
  curl -s -o /dev/null -m 40 -w "%{speed_download}" \
    "https://speed.cloudflare.com/__down?bytes=50000000" \
    | awk '{printf "  %6.1f Mbps", $1*8/1000000}'
  system_profiler SPAirPortDataType | grep -m1 'Signal / Noise'
  sleep 20
done

On settings that had not been touched:

  146.2 Mbps   -80 dBm / -95 dBm  MCS 1
   56.9 Mbps   -79 dBm / -91 dBm  MCS 2
  174.1 Mbps   -79 dBm / -94 dBm  MCS 2
  178.9 Mbps   -79 dBm / -95 dBm  MCS 2

Throughput ranged from 57 to 179 Mbps with nothing altered. The second row is worth reading closely: when throughput collapsed to a third, the noise floor had risen four decibels. Something nearby transmits intermittently, and while it does the link halves.

Across the whole session, on settings that were at various points identical, throughput was recorded between 57 and 191 Mbps. Every comparison after the first one had been measuring that variation rather than the change.

The ruleBefore attributing a difference to a change, measure the unchanged system enough times to know its spread. If the spread exceeds the difference, nothing has been learned yet. Four minutes of sampling replaces an hour of confident wrong conclusions.

The subtler trap is that a single measurement taken after a change always arrives with an explanation attached. The number moved, the alteration is known, and the mechanism is easy to narrate. Narrowing a channel should improve signal-to-noise, so a first reading that agrees invites no further questions. Plausibility is not evidence, and a mechanism that can be explained makes a spurious result more persuasive rather than less.

RTS, CTS, and the hidden node

One more setting looked well matched to the situation, and understanding why requires a short detour into how Wi-Fi avoids collisions.

Wi-Fi shares one channel among every station on it, so before transmitting a station listens and waits if the channel is busy. This is carrier sensing, and it works only when every station can hear every other station. In practice they often cannot. Two clients on opposite sides of an access point may each hear the access point clearly while being deaf to one another. Both sense an idle channel, both transmit, and the two signals collide at the access point. This is the hidden node problem, and radios cannot detect it the way wired Ethernet detects collisions, because a radio cannot listen while it transmits. The sender learns only from a missing acknowledgement, then backs off and retries. The airtime is simply lost.

RTS stands for Request to Send, and CTS for Clear to Send. Together they form a reservation handshake that predates every modern Wi-Fi standard and remains in all of them. Before sending a large frame, a station transmits a short RTS frame stating how long it needs the channel. The access point answers with a CTS frame carrying that same duration. Because the CTS comes from the access point, every station within its range hears it, including the ones hidden from the original sender. Each of them sets an internal countdown, the network allocation vector, and stays silent until it expires:

  client A        access point        client B
  (hidden from B)                     (hidden from A)
     |                  |                  |
     |----- RTS ------->|                  |
     |<---- CTS --------|----- CTS ------->|   B hears it, stays quiet
     |                  |                  |
     |===== data ======>|                  |   channel reserved
     |<---- ACK --------|                  |

The cost is two extra frames for every protected transmission, and they are sent at the lowest available rate so that every station can decode them. On a clean link that is pure overhead. The handshake pays for itself only when collisions are frequent enough that the retransmissions it prevents outweigh the airtime it consumes.

RTS Threshold is the frame size above which the handshake applies. The default of 2346 bytes equals the maximum 802.11 frame size, so no frame ever exceeds it and the handshake never fires. Setting it to 500 means every frame larger than 500 bytes is preceded by the reservation.

The reasoning for trying it here was specific rather than generic. The flat contains many neighbouring networks the client can hear, several of them stronger than its own access point, and the access point sits far enough away that it plausibly hears a different set. That is the hidden node scenario as described in the standard.

Measured across four runs, the threshold at 500 produced no detectable benefit. It was restored to 2346, on the principle that overhead which cannot be shown to buy something should not be carried.

The remaining switches

OFDMA was disabled. Orthogonal Frequency-Division Multiple Access is the headline Wi-Fi 6 feature: it allows an access point to serve several clients inside a single transmission by dividing the channel into subunits, instead of each device waiting its turn for the whole channel. TP-Link ships it disabled for compatibility with older hardware. Enabling it measured as nothing on a network with one active client, which is the expected outcome, and it remains the correct setting to leave on.

Two settings in the same panel are worth leaving alone deliberately. Airtime Fairness allocates equal airtime rather than equal packets to each client, which protects fast clients from slow ones; on a weak link the local machine is the slow client, so enabling it imposes a cap. And WMM must stay enabled, because 802.11n and everything after it depend on the quality-of-service queues it provides.

The commands, collected

Everything used here, in the order it would be run. None of it requires root.

# Current link: signal, noise, channel, width, MCS
system_profiler SPAirPortDataType \
  | sed -n '/Current Network Information/,/Other Local/p'

# Is the signal steady or drifting? Sample it.
for i in 1 2 3 4; do
  system_profiler SPAirPortDataType | grep -m1 'Signal / Noise'
  sleep 5
done

# What else occupies the band, and how crowded each channel is
system_profiler SPAirPortDataType \
  | grep -E 'Channel: [0-9]+ \(5GHz' | sort | uniq -c | sort -rn

# What the Mac supports, often more than the router offers
system_profiler SPAirPortDataType | grep -A6 'Supported Channels'

# The router, and whether its admin page answers
route -n get default | awk '/gateway/{print $2}'
curl -s -o /dev/null -m 5 -w "%{http_code}\n" http://192.168.0.1

# Link health that a throughput test will not reveal
ping -c 20 -q 192.168.0.1

# Throughput, repeated, because one sample says nothing
curl -s -o /dev/null -m 40 -w "%{speed_download}" \
  "https://speed.cloudflare.com/__down?bytes=50000000" \
  | awk '{printf "%.1f Mbps\n", $1*8/1000000}'

# Saved networks, in preference order
networksetup -listpreferredwirelessnetworks en0

One note on the unified log. macOS records thousands of airportd errors that read alarmingly and mean nothing: a missing Core Data model for its own roaming analytics, repeated on a timer. Twelve hours of logs showed zero roams and zero disconnects while that error fired two thousand times. Query for the events that matter rather than counting lines.

The link finished at -78 dBm with no packet loss, 3.5 ms to the router, and throughput between 130 and 180 Mbps depending on neighbouring activity. The two settings that mattered are corrected. The remaining gap to what the hardware can do is five to eight decibels of distance, and no dropdown closes that. Moving the access point would.

Sources

  1. Apple, Analyse your wireless environment with Wireless Diagnostics
  2. Wi-Fi Alliance, Wi-Fi CERTIFIED 6: OFDMA, MU-MIMO and Target Wake Time
  3. IEEE 802.11, the standard defining RTS/CTS and the network allocation vector
  4. MCS Index reference table for 802.11n, ac and ax
  5. European Commission Decision 2005/513/EC on the 5 GHz band in the EU
  6. TP-Link, Archer AX10 documentation and firmware