Why Wi-Fi slows after CSI is enabled: a measured vow_atf and 40 MHz client investigation

By Wu Jun · Updated 2026-11-02 · 5 min read

This was one of the hardest problems we hit after putting CSI into a real home network. It combined client idle state, a firmware tuning switch and bandwidth negotiation, so a single speed test was easy to misread:

On the same router, once CSI was enabled, one Mac's downlink dropped from 300–400 Mbps to 6–10 Mbps, while its uplink stayed at ~45 Mbps.

Below is the full isolation process, the reproducible trigger conditions and the two fixes. Every number is a first-party measurement (grade E). The public 10-minute CSI dataset [1] does not contain these throughput logs, so this article publishes the experiment order, conditions, result tables and toolkit [2]; the raw speed-test logs stay private because they contain home-network details. The driver involved is the upstream open-source mt76 [3].

Four control experiments ruled out everything else:

Hypothesis Experiment Result
WAN / ISP problem download a domestic mirror from the router itself 264–300 Mbps (391 Mbps with 4 threads) — normal
Wired problem check WAN link negotiation 1000 Mbps full duplex, zero error counters
CSI capture overhead disable CSI (CSI enable: 0) and retest still collapses → not caused by CSI capture
Client TX capability Mac uplink over SSH 188 Mbps, AP-side rx rate 600 Mbps 3SS — normal

Conclusion: only the AP→client (downlink) direction collapses, pinned to VHT-MCS0 1SS (13.5 Mbps PHY).

2. The key clue: it collapses when idle, and does not recover

Watching the client's live rate from the router side:

iw dev <iface> station dump | grep -E "tx bitrate|rx bitrate|signal"

What we saw:

  1. about 1 second after going idle, the AP's transmit rate for that client drops to MCS0;
  2. under continuous traffic it sometimes climbs back to MCS9 (~400 Mbps) — and sometimes never recovers;
  3. ping between client and AP: 0% loss, but RTT jitter up to 243 ms (a power-save / buffering signature).

In the "stuck" state, even a continuous 36-second saturated download stayed at 7–10 Mbps.

3. High-confidence trigger #1: VoW ATF prevents rate recovery

mt76 exposes a firmware feature switch in debugfs:

cat /sys/kernel/debug/ieee80211/phy*/mt76/vow_atf   # default = 1

A/B test (same client, same time window, only this switch changed; each run is "60 s idle, then 6 consecutive downloads"):

vow_atf Results over 6 runs (Mbps) Behaviour
1 (firmware default) 9.5 / 7.3 / 9.5 / 7.4 / 9.6 / 7.2 36 s of continuous download never recovers
0 (disabled) 7.8 / 8.8 / 20 / 146 / 387 / 393 climbs back to full speed within ~15–20 s
0 + client re-association 369 / 374 / 376 / 390 / 409 / 397 full speed immediately, 6/6

Reproducible repair switch (made persistent): disable vow_atf and replay the setting at boot and every minute in the watchdog, so it survives driver reloads and reboots:

echo 0 > /sys/kernel/debug/ieee80211/phy*/mt76/vow_atf

The corresponding scripts are published in the stability toolkit, with a SHA-256 checksum.

4. Trigger condition #2: a 40 MHz client inside an 80 MHz BSS

With ATF fixed, a periodic collapse remained. Further A/B testing showed:

Client Width Stability
older Mac (Broadcom, VHT 3SS) 40 MHz collapses repeatedly
phone (HE 2SS) 80 MHz stable at 1200 Mbps
other devices (VHT 1SS) 80 MHz stable

Switching the 5 GHz radio from 80 MHz to 40 MHz (htmode=HE40):

Setting 60 s idle, then 6 consecutive downloads
HE80 (80 MHz) 7.4 / 7.3 / 9.3 / 9.5 / 9.6 / 9.6 Mbps (all collapsed)
HE40 (40 MHz) 384 / 375 / 328 / 416 / 411 / 419 Mbps (6/6 stable)

Conclusion: a client that only negotiates 40 MHz is incompatible with the 80 MHz BSS in this MT7986 + mt76 firmware combination. Native-width operation makes the problem disappear. Because we did not instrument the firmware's internal rate state, we stop at the reproducible condition and repair rather than claiming a specific PPDU field or register-level root cause.

These are grade E field results. The method and repair scripts are in the public toolkit [2]; the raw throughput logs are not part of the public CSI dataset [1].

5. Choosing between the two fixes

Goal Setting Cost
keep high-resolution CSI (80 MHz / 256 subcarriers) stay on HE80 the 40 MHz client is slow
keep daily usability (client at full speed) switch to HE40 CSI bandwidth and subcarrier count halve

A ~30-second switch script lets you move between them on demand:

sh /usr/bin/wifi-htmode HE80   # CSI experiment mode
sh /usr/bin/wifi-htmode HE40   # daily mode

6. A decision flow you can reuse

Wi-Fi got slow
  ├─ only downlink slow, uplink fine? ──▶ check the AP-side tx bitrate for MCS0
  │     ├─ collapses when idle, never recovers ──▶ disable vow_atf (and persist it)
  │     └─ still collapses periodically ───────▶ compare client width vs BSS width
  ├─ both directions slow ──▶ look at interference / channel / negotiation (not covered here)
  └─ wired is slow too ─────▶ check WAN negotiation and ISP (WAN stayed 1000 Mbps in our case)

If your router is also MT7986/MT7915 class (OpenWrt + mt76), run the two tables above first — five minutes tells you which of the two cases you are in.

7. Thirty-minute minimum reproduction

  1. Record router model, OpenWrt/mt76 version, Wi-Fi firmware MD5, channel, width, country and TX power;
  2. record client model, negotiated width, spatial streams, RSSI/SNR and power-save settings;
  3. test uplink, downlink and wired paths to confirm the problem is AP-to-client only;
  4. sample tx bitrate / rx bitrate repeatedly rather than trusting one reading;
  5. idle for 60 seconds, then run six 6-second LAN downloads, recording PHY rate, achieved throughput and bytes on both ends;
  6. change only vow_atf and repeat step 5;
  7. restore vow_atf, change the BSS from HE80 to HE40 and repeat step 5;
  8. restore the original configuration and keep timestamps, command output and client state for every step.

Suggested minimum result table:

Experiment Client / BSS Six achieved downlinks Conclusion
baseline 40 / 80 MHz 7-10 Mbps reproduced
ATF off only 40 / 80 MHz 8 to 393 Mbps ATF correlates with recovery
native width 40 / 40 MHz 328-419 Mbps wide-BSS condition removed

8. Evidence boundary

9. Reporting template

router: GL-MT6000 / OpenWrt ... / mt76 ...
firmware: mt7986_wm.bin MD5 ...
client: model / chip / VHT or HE / NSS / negotiated width
radio: band / channel / width / country / txpower
problem: downlink only? idle trigger? persistent?
baseline: 6x6s results, tx/rx bitrate, bytes AP TX vs client RX
vow_atf=0: 6x6s results
HE40: 6x6s results
restored state: yes/no

Without these fields, maintainers cannot separate driver, firmware, client power-save and environmental effects.

Related: 80 MHz vs 40 MHz · mt76 rate control deep dive · OpenWrt Wi-Fi slow after idle · router CSI guide

References and data sources

Every external claim carries a source and an evidence grade (grading scheme). Grade E marks first-party, reproducible measurements from this project.

  1. Grade E · first-party measurement This project's 10-minute raw capture (57,293 records / 600.0 s, SHA-256 00373f83…95e2)
    Router CSI Lab · 2026
    okwujun.pw/en/datasets/ · verified: Reassembled chunks verified byte-identical (2026-10-11) · The local raw master has SHA-256 00373f83…95e2; the public de-identified derivative has SHA-256 527466ce…a440. They must not be confused.
  2. Grade E · first-party measurement Router CSI Toolkit — capture/analysis/watchdog scripts (MIT, public repository)
    Router CSI Lab · 2026
    github.com/jjjwu/router-csi-toolkit · verified: GitHub API verified the public repository, capture-quality preflight, successful CI and the v0.1.3 release on 2026-11-02; the archive SHA-256 is 59f144c0…adea7 · The public repository includes a dependency-free capture preflight; the browser checker uses the same container definition and evidence boundaries.
  3. Grade C · official docs openwrt/mt76 — mac80211 driver for MediaTek Wi-Fi chipsets
    OpenWrt project / MediaTek
    github.com/openwrt/mt76 · verified: GitHub API 2026-10-11 (stars 890, last push 2026-10-08) · Upstream of the open-source mt76 driver used here; the CSI patch is applied on top of this codebase.