Instrumenting a Closed Wireless Stack: Falsification Discipline and Two Instrument Errors

An in-house investigation with no client attached. Over eight days we instrumented a consumer Wi-Fi 7 router that reported itself healthy while delivering a third of its wired upload rate over the air. We did not fix it. We bounded it: here is what it is not, with the counter that closed each door, and here is precisely what remains unknown.

We are publishing it because two of the errors were ours, and because the finding that survived them did so for a reason worth writing down.

1. A symptom is not yet a claim

The investigation began with a sentence that cannot be tested: the router is slow on upload. Slow against what, measured how, under which conditions held fixed? A sentence in that shape can absorb any evidence. It has no failure condition, so no measurement can contradict it, so no measurement can support it either.

The first work of any diagnosis is to convert the complaint into a proposition that could turn out to be false. Ours became:

On this router, wireless clients are capped at a throughput ceiling on upload that wired clients on the same router, the same WAN path and the same minute do not hit, and the ceiling is independent of client vendor, radio band and 802.11 generation.

Every clause in that sentence is a thing that can fail. If a wired client hit the same ceiling, the claim dies. If one vendor's client cleared it, the claim dies. If it moved with band, the claim narrows to a band problem. That is what makes it worth measuring.

2. The control is built before the hypothesis

Before instrumenting anything we fixed the comparison. A wired client and a wireless client, on the same router, through the same physical WAN path, minutes apart:

GPON ONT (service cabinet)
-> in-wall structured cabling
-> unmanaged gigabit switch
-> router, 10G WAN/LAN-1 port

This matters more than it looks. Because both legs traverse an identical path from the router outward, the carrier is eliminated by construction rather than by argument. We never had to reason about the ISP, model its policer or believe its marketing. Any difference between the two legs is downstream of the router's WAN port by definition.

That single decision retired an entire class of hypothesis before a byte was measured.

The complaint originally arrived as a comparison against a neighbour's older phone on a cheaper router. We kept it in the write-up and labelled it precisely: it is what caused us to start measuring, and it supports nothing. A different flat, a different router, a different client, no controlled variables. An anecdote may be a good reason to open an investigation and a bad reason to close one.

The second control was breadth. The ceiling was reproduced on six devices from four manufacturers — two Macs, three iPhones, a Samsung handset and an Xbox — across 2.4, 5 and 6 GHz, on 802.11ax and 802.11be. A defect in one client's radio, one driver, one band or one standard cannot produce that. Anything specific to a single client was eliminated in the same stroke.

3. Verify the instrument before you read the data

This is the part we would want a client to take away, and it is where we made our first error.

The Broadcom driver exposes channel occupancy through wl -i <interface> chanim_stats. The output is a bare table with no header:

chanspec tx inbss obss nocat nopkt doze txop goodtx badtx glitch badplcp knoise idle timestamp

Column semantics, on this build, indexing from one:

# | Field | Meaning
2 | tx | share of time this AP was transmitting
3 | inbss | share of time occupied by traffic inside this BSS — this is the clients' uplink
4 | obss | traffic from overlapping neighbouring networks
5 | nocat | energy detected but not decodable as 802.11
8 | txop | transmit opportunities remaining, i.e. headroom
11 | glitch | receive glitches
12 | badplcp | PLCP header errors
14 | idle | share of time the channel was idle
15 | busy | share of time the channel was busy

We initially read column 14 as busy. Column 14 is idle. Every conclusion drawn from that pass was therefore exactly inverted — a saturated channel read as an empty one and the reverse. The numbers were correct; the labels were not; and a wrong label on a right number is more dangerous than a wrong number, because it survives every sanity check you apply to magnitudes.

It was caught by an assertion that had nothing to do with the hypothesis: an idle channel must read as idle. We began sampling the interface with no load at all, three times, before every measured run. When "busy" came back at 87 % on a channel with nothing on it, the instrument was wrong, not the world.

Two operational rules came out of this, and both now apply to our client work:

Calibrate against a state whose answer you already know. Not against the state you are investigating — you have no independent knowledge of that one. Idle is the free calibration point in almost every system: an empty queue, a quiescent channel, a table with no rows. If the instrument cannot report the trivial case correctly, nothing it says about the interesting case is worth reading.

Distrust undocumented positional output. A headerless table of fifteen integers is a format that will eventually be misread. Where a system offers positional output and no schema, the schema has to be reconstructed and verified before it is used, and the reconstruction has to be written down next to the data.

We also verified that the tooling was pointed at the right object. Clients on this router are associated to a virtual BSS, wl1.1, not to the primary interface wl1. Per-station statistics queried against wl1 return successfully and describe nothing. A tool that returns zero rows without erroring is a tool that will quietly confirm any hypothesis you bring to it.

4. Elimination in order of cost

With a verified instrument, hypotheses were closed cheapest-first. Each row is a door, and the counter that shut it:

Hypothesis | Closed by
Carrier or line policer | Wired leg over the identical path reaches 320–392 Mbps
Client-side radio or driver | Six devices, four vendors, three bands, two 802.11 generations
Router configuration | Survives a full factory reset and manual rebuild
Firmware build | Two builds, including a deliberate six-month downgrade
CPU saturation | 4–5 % per core throughout, from /proc/stat deltas
Forwarding path / software slow path | Flow cache 100.0 % hardware-accelerated in both directions, slow path below 0.05 %
Latched hardware state | Ten-minute mains disconnect: 136.5 → 136.0 Mbps
TCP ACK prioritisation | Clean A/B, enabled vs disabled: no difference
RF interference or a noisy channel | 361 retried frames of 18.5 M on the client uplink (0.002 %); badplcp and glitch at zero in every sample
Buffer exhaustion at the sender | TCP send buffers from 128 KB to 8 MB: unchanged
MTU, PMTU, MSS clamping | Verified and adjusted, no effect
WAN port, cabling, switch | Three ports, unmanaged switch inserted inline
Aggregation, OFDMA, MU-MIMO, beamforming, TWT, APSD | Each toggled individually and confirmed in the driver, not in the web UI

Two of these deserve comment.

The flow-cache result is the strongest single elimination in the set. Both directions resolve to hardware-accelerated paths — wl1.1_rx → br0_rx → ppp0_tx outbound and ppp0_rx → br0_tx → wl1.1_tx inbound — at 100.0 %, with the software slow path below 0.05 % of packets. Whatever limits the uplink is not the forwarding path and is not the processor. Both were plausible; both are now closed with a counter rather than an opinion.

The TCP ACK prioritisation A/B was an accident worth keeping. We had disabled the setting during earlier testing; the cold power cycle reset it to enabled. That made the before-and-after pair a clean controlled comparison on the single setting most often recommended for this symptom. It made no difference. An accidental experiment is still an experiment, provided you notice it happened and say so.

Note also what is not in that table: the settings were verified in the driver over SSH rather than in the management interface, because several of the relevant controls are not exposed in the UI on this model, and a control that is not exposed cannot be confirmed by looking at the page that does not show it.

5. The hypothesis that felt right and was wrong

By the middle of the investigation we had an explanation we liked. It ran roughly: throughput equals acknowledgement rate multiplied by bytes acknowledged per acknowledgement, the acknowledgement rate on the uplink is constrained, therefore the uplink is clocked by acknowledgements arriving from across the WAN.

It had an equation. It matched the numbers. We drafted it, and we put it in front of five independent reviewers whose brief was to destroy it.

They did, on two grounds, and both are instructive.

The equation was a conservation identity, not a mechanism. Throughput is defined as acknowledgement rate times bytes per acknowledgement. The relation cannot fail. It holds in a healthy system and a broken one, at every rate, on every medium. A statement that cannot be false carries no information, and dressing it in notation does not change that. It had felt like evidence precisely because it was always true.

The data pointed the other way. The download direction carries fewer acknowledgements per second and achieves higher throughput. Under our own story that is impossible. The measurement we were using as support was in fact a refutation, and we had not looked at it that way because we had stopped looking once it agreed with us.

Eighteen specific claims were retired. The draft was pulled before publication, the retraction was written into the public thread rather than quietly edited out, and the support case filed with the manufacturer contains none of it.

The general form of the error is worth naming, because it is common and it is expensive: a hypothesis with no failure condition is not a hypothesis. Before accepting an explanation, the question is not "does this fit the data" but "what observation would have made this false, and did we go and look for it".

6. The second instrument error, and what it did not touch

The same review pass found that our throughput figures had been computed against an assumed 45-second measurement window when the true elapsed intervals were 62 and 47 seconds. Correcting it moved the headline numbers substantially:

| First pass | Corrected
Upload | 136.5 Mbps | 129.0 Mbps
Download | 512 Mbps | 231.9 Mbps
Ratio | 3.7× | 1.8×

A two-fold error in the most quotable number in the investigation.

And it changed the finding not at all — because the finding never rested on those numbers. The core result is an airtime comparison, and airtime shares are read directly from channel-utilisation samples. They are not derived by dividing a byte count by an assumed duration, so an error in the assumed duration cannot propagate into them.

That is the most transferable thing in this document. When a number moves, the question is not how large the correction was but which conclusions were load-bearing on it. Most analyses do not know. Ours did, because the elimination table above had been built one counter at a time, and each row records what it depends on.

If a client asks us which of their metrics an executive decision actually rests on, and the answer is not immediate, that is the finding.

7. What survived

Three scenarios, same client, same association, same AC_BE traffic class, same TXOP limit, minutes apart on a 45-second instrumented run. Occupancy from chanim_stats, aggregation depth from wl counters deltas as Δtxmpdu / Δtxampdu:

Scenario | AP A-MPDU depth | busy | txop free | AP tx | client uplink (inbss)
Download from internet | 27.8 | 96 % | 5 % | 91 % | 3 %
Upload to internet | 3.5 | 48 % | 53 % | 15 % | 27 %
Upload to the router itself | 9.6 | 95 % | 5 % | 8 % | 83 %

Read the last column. Sending to the internet, the client occupies 27 % of the airtime. Sending to the router itself — same radio, same association, same encryption, minutes later — it occupies 83 %, and drives the channel to 95 % busy.

The radio will take the channel to saturation with this client's uplink traffic. It does not do so when the destination is past the WAN port. Contention, EDCA parameters and TXOP limits are identical in both runs and therefore cannot account for the difference. An independent earlier measurement by a different method — a raw TCP stream to a listener on the router, 3 GB over 75 seconds — put the local figure at 327 Mbps, corroborating the direction if not the exact magnitude.

The aggregation column adds a detail. The AP's downlink aggregate is 27.8 MPDUs when carrying data, 3.5 when carrying nothing but acknowledgements for an internet-bound upload, and 9.6 when acknowledging the local upload. The shallow acknowledgement aggregation therefore looks like a consequence of acknowledgements trickling back through the WAN, not an independent cause. We note it as a lead, not a conclusion.

The plain statement of the result is this: in one direction the air is largely idle and something is failing to fill it.

8. What we are not claiming

We have not demonstrated a firmware defect. We have a measurement we cannot explain, which is a different and smaller thing.

These are access-point-side counters. On upload the client is the transmitter, so the picture is asymmetric by construction. An over-the-air capture with MU-EDCA parameters visible would close that gap. We did not take one.

One unit, ours. Nothing here establishes that it reproduces on another.

Every figure is a single run. Reproducible to within a few percent across roughly thirty-five runs in aggregate, but not statistically treated.

obss reads zero in every upload sample, which is surprising in a high-rise. The BSS runs on channel 100 in the DFS band, which most consumer equipment avoids, and that is the likely explanation — but we captured no band scan to corroborate it, so it stands as an assumption rather than a result.

Stating the limits is not modesty. An investigation that does not publish its own weakest points has not finished looking for them.

9. The discipline, stated plainly

Five rules, each of which cost us something in this investigation:

Convert the complaint into something that can be false. If no observation could contradict the statement, no observation can support it.

Build the control before the hypothesis. Eliminating the carrier by routing both legs through an identical path is worth more than any amount of subsequent reasoning about the carrier.

Calibrate the instrument against a known state. We inverted a channel-occupancy reading and would have published it. Idle is free calibration; use it every time.

Reject explanations that cannot fail. Our best-liked hypothesis was a conservation identity in costume. Ask what observation would refute it, then go and look for that observation specifically.

Know which conclusions load-bear on which numbers. A 2× correction that changes nothing is a well-built analysis. A 5 % correction that changes everything is a fragile one. Most teams discover which they have only after the correction arrives.

The same argument runs through our client work, where the substrate is a transaction graph or a pricing model rather than a radio: an invariant you have verified is worth more than a model you have trained, and a system you cannot explain is a system you do not control.

10. Reproduction

Everything behind this write-up is public. Station MAC addresses are pseudonymised and public IPs removed; no lines were deleted, edited or reordered.

Raw instrumentation — chanim_stats and bs_data captures, nvram and driver state, interface counters across 37 samples, full kernel log, A-MPDU and per-core CPU measurements: gist.github.com/vdatanexus/435dac22945b90dd3af30a4d94bc73ea

Primary discussion threads, where the retraction and the peer challenges are visible in sequence: SNBForums and ASUS ZenTalk

Vendor case ASUS support request E26080002665-0001, filed 3 August 2026, including a request to publish GPL sources for this SKU as was done for the AI variant. Open at time of writing.

The investigation remains open. If you own this hardware and can reproduce or refute the airtime asymmetry, the threads above are the place to say so, and a contradiction is more useful to us than a confirmation.