Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

netdrop

OBS says Dropped frames (network). Task Manager shows a throughput graph that looks fine. Neither one tells you why.

Windows already measures the number that explains it — the TCP retransmission rate — and shows it in no user interface anywhere. netdrop shows that number, plus the per-adapter discards and errors behind it, and the gap between the link speed Windows advertises and what you can actually push.

Read-only. Zero dependencies. One PowerShell file. No admin rights.

This is not a speed test and not a packet capture. It sends no traffic of its own. It reads counters Windows is already keeping and explains them.

powershell -NoProfile -ExecutionPolicy Bypass -File netdrop.ps1 -Adapters
  netdrop - why your stream drops frames when nothing looks wrong
  ══════════════════════════════════════════════════════════════

  LINK
    Local Area Connection   Xbox Wireless Adapter for Windows
      link speed Windows shows    600.0 Mbps
      link speed right now        600.0 Mbps

    Wi-Fi   Intel(R) Wi-Fi 6 AX201 160MHz
      link speed Windows shows    721.0 Mbps
      link speed right now        400.0 Mbps   ← renegotiated since
      802.11ax  5 GHz  ch 153  signal 81% (-64 dBm)  SSID 5roof-kids

    Bluetooth Network Connection   Bluetooth Device (Personal Area Network)   [Disconnected]
      link speed Windows shows      3.0 Mbps

  WHAT YOU CAN ACTUALLY PUSH
    A link speed is not throughput. Wi-Fi is half duplex on a shared
    medium, so real TCP goodput lands near half the PHY rate before any
    interference. Windows shows the same number for both.

    Local Area Connection
      link rate now         600.0 Mbps
      realistic goodput    ~564.0 Mbps
      4K60 stream needs      40.0 Mbps   OK  14.1x headroom

    Wi-Fi
      link rate now         400.0 Mbps
      realistic goodput    ~200.0 Mbps
      4K60 stream needs      40.0 Mbps   OK  5.0x headroom

  RETRANSMISSIONS   measured by Windows, shown by nothing
    TCPV4     1,167 of     706,532 segments resent     0.165%  ░░░░░░░░░░
            right now: 0.0 resent/s of 1.0 sent/s
    TCPV6        60 of         116 segments resent   not measurable
            below the 1,000 segment floor - a percentage from that few
            samples is noise, so no rate is reported
            right now: 0.0 resent/s of 0.0 sent/s

    Every resent segment is data that had to cross the network twice.
    A live encoder cannot wait for it, so OBS calls it a dropped frame.

  ADAPTER ERRORS AND DISCARDS   since boot
    Local Area Connection
      outbound          0 discarded          0 errors
      inbound           0 discarded          0 errors
      send queue        0 packets waiting
      totals     0 B sent   0 B received
    Wi-Fi
      outbound          0 discarded          0 errors
      inbound           0 discarded          0 errors
      send queue        0 packets waiting
      totals     93.61 MiB sent   131.11 MiB received

  CONNECTIONS   since boot, IPv4 and IPv6 combined
    opened          1,177
    failed             58   4.9% of attempts
    reset             149
    open now            9

  LIVE   over 5.2 s
    Local Area Connection    up        0 B/s   down        0 B/s
    Wi-Fi                    up      188 B/s   down      285 B/s

  VERDICT
    [info] TCPV6 has not sent enough traffic to measure
            Only 116 segments since boot, below the 1,000 floor. A percentage
            from that few samples is noise, so no rate is reported.
    [info] Wi-Fi link speed is not what Windows is showing
            Get-NetAdapter says 721.0 Mbps but the live performance counter
            says 400.0 Mbps. On Wi-Fi the PHY rate renegotiates continuously,
            so the number in the status dialog is a snapshot, not a property
            of the link.

That is one unedited capture from the machine this was built on, not a mock-up and not stitched together from several runs.


Why the graph looks fine while your stream falls apart

Because throughput is an average and a stall is not. A 6 Mbps upload sitting on a 100 Mbps link looks like nothing on a graph. But a retransmitted segment is data that had to cross the network twice, and the sender waits for a timeout before it tries again. A file download absorbs that invisibly — it just finishes a moment later. A live encoder cannot wait. The frame is already stale by the time the retry lands, so it gets thrown away, and OBS increments the dropped-frame counter.

Because Windows measures retransmissions and shows them nowhere. The counter is right there in Win32_PerfRawData_Tcpip_TCPv4. It is not in Task Manager, not in the Wi-Fi status dialog, not in Settings, and not in Resource Monitor. netdrop is mostly a window onto a number the operating system already has.

Because "link speed" is not a speed you will ever get. Wi-Fi is half duplex on a shared medium: every device on the channel takes turns, and the acknowledgements come back over the same air. Real TCP goodput lands near half the PHY rate before you account for a single neighbour. Windows shows one number for both and calls it "Speed".


Four things this found that Windows will not tell you

1. The Wi-Fi link speed Windows shows you is stale

In the capture above, Get-NetAdapter reported 721.0 Mbps while the live performance counter said 400.0 Mbps at the same instant. Across probes minutes apart on an idle machine the live value moved 865.0 → 648.5 → 600.5 → 576.5 → 432.0 → 721.0 → 400.0 Mbps, while Get-NetAdapter.Speed and Win32_NetworkAdapter.Speed lagged behind it every time.

The Wi-Fi PHY rate renegotiates continuously with signal quality. The number in the connection status dialog is a snapshot from some past moment, not a property of the link. netdrop shows both and flags the gap.

Source Value
Get-NetAdapter.Speed — what Windows shows you 721,000,000
NetworkInterface.CurrentBandwidth — what it is right now 400,000,000

netsh wlan show interfaces is a third, completely separate source, and it tracks the live counter rather than the stale one — realcheck.ps1 reads all three every run and prints them. It deliberately does not assert that they are equal: they are three samples of a continuously renegotiating value taken at three different instants, so demanding equality would be asserting a flaky claim rather than testing a parse.

2. A percentage from a tiny sample is a spectacular false alarm

In the capture above, IPv6 had sent 116 segments since boot and retransmitted 60 of them. Reported naively that is 51.7% packet loss — a screaming red alert on a perfectly healthy connection that has essentially never been used. An earlier probe on the same machine caught it at 40 of 76, which is 52.6%.

netdrop refuses to report a rate below 1,000 segments and says why. The field is left null, never 0, because "unknown" must not be able to masquerade as "zero, therefore healthy".

3. The same adapter is spelled three different ways by three Windows APIs

API Spelling
Get-NetAdapter Intel(R) Wi-Fi 6 AX201 160MHz
WMI perf classes Intel[R] Wi-Fi 6 AX201 160MHz
PDH / Get-Counter samples intel[r] wi-fi 6 ax201 160mhz

PDH instance names cannot contain ( ) # / \, so the performance subsystem rewrites them — and lowercases them in returned samples while the counter set listing shows original case. Route every name through one canonical formatter or half your data silently attaches to nothing.

4. Persec in a WMI performance class does not mean "per second"

SegmentsSentPersec on this machine reads 706,532. That is not a rate — it is the cumulative count since boot. Every *Persec field in Win32_PerfRawData_* is a raw counter and you must difference two samples yourself. The name lies.


How the numbers are verified

Three suites ship with the tool. All three are run before every release and all three must be green.

powershell -NoProfile -ExecutionPolicy Bypass -File selftest.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File mutate.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File realcheck.ps1

selftest.ps1 — 265 assertions against synthetic ground truth

Every field of every fixture carries a different distinctive value, so a tool that mixes two fields up cannot pass by coincidence. Both sides of every threshold are tested, including the exact boundary. A healthy fixture must raise zero risks, so the tool cannot pass by crying wolf about everything.

mutate.ps1 — 58 deliberate bugs, 58 caught

A suite that passes proves nothing unless it would fail on a broken tool. This introduces one real bug at a time and requires selftest.ps1 to catch every one.

  mutation score: 58 / 58 killed
  invalid controls: 0

  excluded from the score:
    - measurable flag always true  (no-op by construction)

Three things it reports separately, because conflating them would be dishonest:

  • survivors — the mutation was introduced and the suite still passed. An untested code path.
  • invalid controls — the anchor text was not found in the source, so nothing was mutated. An unvalidated anchor silently tests nothing and would otherwise be reported as a pass. Two were caught this way during development.
  • excluded no-ops — a mutation that provably cannot change behaviour is excluded from the score rather than counted as a pass.

It supports paired edits for equivalent mutants. Format-Bar clamps both the fraction and the derived fill count; removing either alone changes nothing observable, so the harness removes both together instead of the assertion being deleted.

realcheck.ps1 — 51 checks against live system data, every one with a negative control

Synthetic fixtures are clean and predictable, which is exactly why they miss things. This runs the same parsers against genuine live data and cross-checks every claim against an independent reference obtained by a different technique.

  R1b  every network counter: PDH (pdh.dll) vs the WMI perf provider
    [ok]   compared 89 live counter fields across 6 counter sets, 0 mismatches
           (85 static compared EXACTLY, 4 monotonic bracketed, 0 gauges excluded)
    [ok]   negative control on static fields: 11 of 11 mutations rejected
    .      [UNDETECTABLE] 32-bit truncated - no static field it changes on this machine

  R2  rate arithmetic vs PDH cooked values
    .      SecondValue / 1e7:       mine 30482.4092  PDH 30482.4092  relerr 0.00000000%
    .      Timestamp100NSec / 1e7:  mine 30485.0598  PDH 30482.4092  relerr 0.00869522%
    .      Timestamp.Ticks / 1e7:   mine 30485.0598  PDH 30482.4092  relerr 0.00869522%
    [ok]   negative control: 2 of 2 wrong timebases scored worse
    [ok]   negative control: 5 of 5 scale and sign errors rejected

  51 passed, 0 failed, 0 skipped

Every comparison carries a negative control. A check that tolerates anything proves nothing, so after the real comparison passes the identical check is replayed against deliberately corrupted values — off by one, sign inverted, read as kilobytes, read as a fraction, 32-bit truncated, first two digits swapped — and every one must be rejected. 0 mismatches only means something next to 11 of 11 mutations killed.

Fields are classified empirically, not assumed. Three alternating WMI samples decide whether a field is static (never varied → compared exactly, full mutator set including a single-unit change), monotonic (only ever rose → interval- containment bracketing is valid) or a gauge (observed to fall). Gauges are excluded from the exact claim entirely, because an oscillating level can return to a byte-identical reading having genuinely moved, so bracketing cannot establish stillness for them. Claiming "0 mismatches" about data that cannot be compared would be a lie.

Ground truth is planted inside real data. A synthetic adapter record with a distinctive value in every field — including a name containing ( ), # and / specifically to exercise the canonicaliser — is inserted into the middle of a genuine live counter snapshot. The parser must find exactly those values while surrounded by real ones, no real adapter may be displaced, and no planted value may leak into a real record.

The headline feature is verified by generating the condition it detects. Do not trust a counter because it returns a number without erroring — a plausible constant is exactly what a broken path returns. realcheck opens 7 loopback TCP connections and requires ConnectionsActive to move by at least 7 (exactly 7 when the machine is otherwise quiet), then confirms the segment counter moved too so it is not a stale constant. Loopback uses no external bandwidth, touches no configuration and kills nothing. -SkipLoad skips it.

The tool's own -Json output is fed back through a bounds check, including aggregates — every percentage finite and within 0–100, no negative rates, retransmitted never exceeding sent, and no adapter exceeding its own link capacity. Replay of a replay must be byte-identical, a UTF-8 BOM on the capture must be tolerated, and the error path must still emit valid JSON.

Read-only, and proved rather than asserted

realcheck.ps1 scans the source for 33 mutating calls (Set-ItemProperty, Set-NetAdapter, Invoke-CimMethod, Stop-Service, netsh interface set, …) and requires none of them, then proves the scanner works by injecting one and requiring it to be found. It then snapshots the real adapter and IP configuration before and after a full run and requires the result to be byte-identical, with a negative control proving the snapshot detects a one-byte change.

There is no undo because there is nothing to undo.


Two Windows traps this had to work around

A single Get-Counter sample cannot measure a rate — and it is worse than returning zero. PDH cooks a /sec counter against whatever previous raw sample it happens to be holding, which came from an arbitrary earlier query. So -MaxSamples 1 gives you either a flat 0 or a rate over an interval you do not control, and the second case is more dangerous because it looks perfectly plausible. Measured live:

  single sample over the caller's own 4.0 s span:
    PDH cooked 0.0000, caller's actual rate 2.4788  (100.00% off)

netdrop takes two explicit samples and does its own arithmetic.

The rate formula was derived empirically, not from documentation. PDH exposes RawValue and SecondValue alongside CookedValue, so running your own arithmetic over two consecutive raw samples covers the identical window PDH used and must agree to the digit:

rate = Δ(RawValue) / (Δ(SecondValue) / 10,000,000)

CounterSample.SecondValue is the PerfTime timestamp, not a second data value. That form reproduces PDH's own cooked value to 0.00000000%, while Timestamp100NSec and Timestamp.Ticks are both measurably worse. realcheck.ps1 reruns that comparison and prints all three every time, and requires the two wrong timebases to score worse — otherwise the comparison is not discriminating and the result means nothing.


Options

Flag What it does
-Seconds <n> Length of the live sampling window. Default 5.
-Adapters Include every adapter, not just the ones carrying traffic.
-Info Explain where each number comes from and how it is computed.
-Json Machine-readable output. Nothing else is printed.
-Quiet Suppress all output; use the exit code.
-FromJson <path> Replay a saved -Json capture instead of reading the machine.

Exit codes: 0 clean, 1 risks found, 2 error.

-FromJson is the useful one for helping someone else. Have them run netdrop.ps1 -Json > net.json and send you the file — you get their entire network picture, replayable, with no screenshots and no remote session.


Requirements

Windows PowerShell 5.1 or later. Nothing else — no modules, no downloads, no admin rights.

Execution policy. Windows blocks unsigned .ps1 files by default. Run it with -ExecutionPolicy Bypass as shown above; that is process-scoped and changes nothing on your machine. Do not change your machine's policy for this.

Runs fine without administrator rights. Everything it reads is available to a standard user.


See also

Built by the same hand, same rules: read-only where it can be, dry-run where it cannot, no dependencies, and every number cross-checked against a second source.

  • obs-4k60-recorder — record gameplay at native resolution and 60 fps with hardware encoding
  • framecheckyour recording already dropped frames; this finds them
  • gpucheck — whether the GPU was why
  • diskrate — whether the disk was why
  • cpuclock — whether the CPU clock was why
  • ramcheck — whether memory pressure was why
  • miccheck — audio desync, quiet mics and ducking
  • bootlag — what is slowing your boot, in milliseconds
  • gamemode — free the machine for a session, then put it all back
  • pathfix — why the wrong ffmpeg runs

License

MIT © 2026 appsmypass

About

OBS says 'dropped frames' and Task Manager shows a throughput graph; neither tells you why. Shows the TCP retransmission rate Windows measures but never displays, per-adapter discards and errors, and the gap between the Wi-Fi link speed Windows advertises and what you can actually push. Read-only, zero dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages