DEV Community

Scott Williams
Scott Williams

Posted on

Wi-Fi keeps dropping on Windows 11? Find out where with PowerShell before you reset anything

When Wi-Fi keeps dropping, the usual advice is a list of resets: forget the network, restart the router, netsh winsock reset, reinstall the driver, Network reset. Microsoft's own "Fix Wi-Fi connection issues in Windows" page lists them in that kind of order, and it's a good list. Start with the Network and Internet troubleshooter in the Get Help app.

But Microsoft's troubleshooting guide for IT pros makes a point that's easy to skip: collect a baseline and note when the problem happens before you reset the network stack, delete wireless profiles or remove the driver. And there's one question a reset never answers: is the drop between your PC and your router, or past the router? If it's past the router, no amount of resetting your PC will help.

This post builds a small, read-only PowerShell file that answers that, in four parts:

  1. What the adapter says right now (netsh wlan show interfaces, as objects).
  2. What Windows logged when it dropped (disconnect events with their reason text).
  3. Whether Windows may power the adapter off to save battery.
  4. A ping log to your router and, optionally, to an internet address, with a summary that tells you where the drops are.

None of it changes a setting. The only file it writes is the CSV log you name.

1. The link, as objects

netsh wlan show interfaces prints a Key : Value block per Wi-Fi interface: state, SSID, the access point's BSSID, band, channel, radio type, receive and transmit rates, and signal as a percentage. That's fine to read, but awkward to log. A small parser turns it into objects:

function ConvertFrom-WlanInterfaceText {
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string[]] $Text)

    $map = [ordered]@{
        'Name' = 'Name'; 'Description' = 'Description'; 'State' = 'State'
        'SSID' = 'SSID'; 'AP BSSID' = 'BSSID'; 'BSSID' = 'BSSID'
        'Radio type' = 'RadioType'; 'Authentication' = 'Authentication'
        'Band' = 'Band'; 'Channel' = 'Channel'
        'Receive rate (Mbps)' = 'ReceiveMbps'; 'Transmit rate (Mbps)' = 'TransmitMbps'
        'Signal' = 'SignalPercent'; 'Profile' = 'Profile'
    }
    $current = $null
    foreach ($line in ($Text -split "`r?`n")) {
        if ($line -notmatch '^\s+(?<key>[^:]+?)\s+:\s?(?<value>.*)$') { continue }
        $key = $Matches.key.Trim()
        $value = $Matches.value.Trim()
        if ($key -eq 'Name') {                      # a new interface starts
            if ($current) { [pscustomobject]$current }
            $current = [ordered]@{}
            foreach ($p in ($map.Values | Select-Object -Unique)) { $current[$p] = $null }
        }
        if ($null -eq $current -or -not $map.Contains($key)) { continue }
        $prop = $map[$key]
        switch ($prop) {
            'SignalPercent' { $current[$prop] = [int]($value -replace '[^\d]', '') }
            { $_ -in 'Channel', 'ReceiveMbps', 'TransmitMbps' } {
                $n = 0.0
                if ([double]::TryParse($value, [Globalization.NumberStyles]::Float,
                        [Globalization.CultureInfo]::InvariantCulture, [ref]$n)) { $current[$prop] = $n }
            }
            default { $current[$prop] = $value }
        }
    }
    if ($current) { [pscustomobject]$current }
}
Enter fullscreen mode Exit fullscreen mode

Three details worth knowing:

  • Two possible labels for the access point. Depending on the Windows build, the line may read AP BSSID or just BSSID. The map accepts both.
  • The regex splits on the first : (space, colon), so a BSSID like aa:bb:cc:dd:ee:01 survives intact.
  • The labels may be localized. On a non-English display language the keys may not match, and then you get nothing back. Get-WifiStatus (the wrapper that actually runs netsh) warns when that happens instead of failing silently.
function Get-WifiStatus {
    [CmdletBinding()]
    param()
    if (-not (Get-Command -Name netsh.exe -ErrorAction SilentlyContinue)) {
        throw 'netsh.exe was not found. This function needs Windows.'
    }
    $raw = & netsh.exe wlan show interfaces
    $found = 0
    foreach ($interface in (ConvertFrom-WlanInterfaceText -Text $raw)) { $found++; $interface }
    if ($found -eq 0) {
        Write-Warning 'No Wi-Fi interface was found in the netsh output. If your Windows display language is not English, run "netsh wlan show interfaces" and read it directly.'
    }
}
Enter fullscreen mode Exit fullscreen mode

2. What Windows logged when it dropped

The WLAN AutoConfig service keeps its own event log: Applications and Services Logs > Microsoft > Windows > WLAN-AutoConfig > Operational. Event 8001 is a successful connection and 8003 is a disconnection. Each 8003 carries the SSID, the adapter and, most usefully, a Reason string and a ReasonCode.

The reason text is in the event's XML, not in its summary message, so read the EventData fields by name:

function ConvertFrom-WlanEventXml {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string] $Xml, [datetime] $TimeCreated, [int] $Id)
    $doc = [xml]$Xml
    $data = @{}
    foreach ($d in $doc.Event.EventData.Data) { $data[[string]$d.Name] = [string]$d.'#text' }
    [pscustomobject]@{
        TimeCreated = $TimeCreated
        Id          = $Id
        SSID        = $data['SSID']
        Adapter     = $data['InterfaceDescription']
        Reason      = $data['Reason']
        ReasonCode  = $data['ReasonCode']
    }
}

function Get-WifiDisconnectEvent {
    [CmdletBinding()]
    param([ValidateRange(1, 90)][int] $Days = 7)
    $filter = @{
        LogName   = 'Microsoft-Windows-WLAN-AutoConfig/Operational'
        Id        = 8003
        StartTime = (Get-Date).AddDays(-$Days)
    }
    $events = @(Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue)
    foreach ($e in $events) {
        ConvertFrom-WlanEventXml -Xml $e.ToXml() -TimeCreated $e.TimeCreated -Id $e.Id
    }
}
Enter fullscreen mode Exit fullscreen mode

-FilterHashtable filters inside the event log service, which is much faster than piping the whole log through Where-Object. -ErrorAction SilentlyContinue matters here: Get-WinEvent throws "No events were found" when the filter matches nothing, and an empty week is a valid answer.

Get-WifiDisconnectEvent -Days 7 | Format-Table TimeCreated, SSID, Reason -AutoSize
Enter fullscreen mode Exit fullscreen mode

Group by Reason to see whether the drops share a cause. A reason that says the driver reported the disconnection points at the adapter side; a reason that says you or a policy disconnected points somewhere else entirely.

One caveat: when a PC sleeps and wakes in range of the same network, Windows may not log an 8003/8001 pair, so a short list doesn't prove there were no drops. If you want the full picture, netsh wlan show wlanreport (run as administrator) builds an HTML report that, in Microsoft's words, summarizes recent wireless sessions, disconnect reasons, adapter information and related events.

3. Is Windows allowed to switch the adapter off?

"Wi-Fi drops after the PC has been idle or after sleep" is often the adapter's power-saving setting: Device Manager > your Wi-Fi adapter > Properties > Power Management > Allow the computer to turn off this device to save power. You can read the same setting with Get-NetAdapterPowerManagement, which needs an elevated window:

function Get-WifiAdapterPowerSetting {
    [CmdletBinding()]
    param()
    $wifi = @(Get-NetAdapter -Physical -ErrorAction Stop |
        Where-Object { $_.PhysicalMediaType -eq 'Native 802.11' })
    foreach ($a in $wifi) {
        $pm = Get-NetAdapterPowerManagement -Name $a.Name -ErrorAction SilentlyContinue
        $allow = if ($pm) { [string]$pm.AllowComputerToTurnOffDevice } else { 'Not reported' }
        [pscustomobject]@{
            Name                         = $a.Name
            Description                  = $a.InterfaceDescription
            Status                       = [string]$a.Status
            AllowComputerToTurnOffDevice = $allow
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This only reports. If you decide to untick the box, do it in Device Manager, where ticking it again is the undo.

4. Router or internet? Log it

This part gives you a timestamped log to show your internet provider. Every few seconds, ping your default gateway (your router) and, optionally, a public address. Write each sample to a CSV along with the signal and the BSSID:

  • Router stops answering → the problem is between your PC and your router (signal, power saving, driver, or the router).
  • Router answers, internet address doesn't → your Wi-Fi is fine; the problem is past the router.

The ping uses .NET's System.Net.NetworkInformation.Ping directly, which should behave the same in Windows PowerShell 5.1 and PowerShell 7 (not yet checked on 5.1; Test-Connection changed its parameters and output between the two):

function Test-PingOnce {
    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)][string] $Target, [int] $TimeoutMs = 1000)
    $ping = New-Object System.Net.NetworkInformation.Ping
    try { return ($ping.Send($Target, $TimeoutMs).Status -eq 'Success') }
    catch { return $false }
    finally { $ping.Dispose() }
}
Enter fullscreen mode Exit fullscreen mode

The gateway comes from the default route with the lowest metric:

function Get-DefaultGateway {
    [CmdletBinding()]
    param()
    $route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
        Sort-Object -Property RouteMetric | Select-Object -First 1
    if ($route) { $route.NextHop }
}
Enter fullscreen mode Exit fullscreen mode

The logger appends one row per sample with Export-Csv -Append, so if you stop it with Ctrl+C, everything logged so far is already on disk:

function Watch-WifiConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Path,
        [ValidateRange(1, 1440)][int] $Minutes = 30,
        [ValidateRange(1, 300)][int] $IntervalSeconds = 5,
        [int] $Samples = 0,
        [string] $Gateway,
        [string] $InternetHost
    )
    if (-not $Gateway) { $Gateway = Get-DefaultGateway }
    if (-not $Gateway) { throw 'No default gateway found. Connect to Wi-Fi first, or pass -Gateway.' }
    if ($Samples -le 0) { $Samples = [math]::Max(1, [int](($Minutes * 60) / $IntervalSeconds)) }
    $haveNetsh = [bool](Get-Command -Name netsh.exe -ErrorAction SilentlyContinue)

    for ($i = 0; $i -lt $Samples; $i++) {
        $wifi = $null
        if ($haveNetsh) { $wifi = Get-WifiStatus -WarningAction SilentlyContinue | Select-Object -First 1 }
        $row = [pscustomobject]@{
            Time       = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
            State      = if ($wifi) { $wifi.State } else { '' }
            SSID       = if ($wifi) { $wifi.SSID } else { '' }
            BSSID      = if ($wifi) { $wifi.BSSID } else { '' }
            Signal     = if ($wifi) { $wifi.SignalPercent } else { '' }
            GatewayOk  = Test-PingOnce -Target $Gateway
            InternetOk = if ($InternetHost) { Test-PingOnce -Target $InternetHost } else { '' }
        }
        $row | Export-Csv -LiteralPath $Path -Append -NoTypeInformation -Encoding UTF8
        $row
        if ($i -lt $Samples - 1) { Start-Sleep -Seconds $IntervalSeconds }
    }
}
Enter fullscreen mode Exit fullscreen mode

Measure-WifiLog then reads the CSV back and counts router failures, "router fine, internet failed" samples, the lowest signal, and how often the BSSID changed (a roam to another access point or band), and prints a one-line verdict. It's in the repo; it's plain counting over Import-Csv.

Run it while the problem is happening:

. .\wifi-drop-diagnostic.ps1
Watch-WifiConnection -Minutes 30 -InternetHost 1.1.1.1 -Path .\wifi-log.csv
Measure-WifiLog -Path .\wifi-log.csv
Enter fullscreen mode Exit fullscreen mode

1.1.1.1 is only an example of a public address that answers pings. Leave -InternetHost out and only your router is pinged.

Things that bite

  • Some routers ignore pings. If GatewayOk is false even when everything works, the logger can't help on that network.
  • One ping every few seconds can miss a very short drop. Lower -IntervalSeconds if you need finer detail; the default of 5 keeps the log small.
  • More than one Wi-Fi adapter: the logger records the first one netsh lists.
  • Non-English Windows: Get-WifiStatus may return nothing and warn. The pings and the event log reader should still work, because they don't read display text, but that hasn't been tested.

How it was tested (and what wasn't)

The script parses cleanly, PSScriptAnalyzer reports no findings (including its Windows PowerShell 5.1 syntax-compatibility rule), and 12 Pester tests pass. Those tests ran under PowerShell 7 on Linux, with hand-written sample text in the netsh layout, a sample event XML, and mocks for the event log, ping and sleep. It has not been run on a real Windows 10/11 PC or on Windows PowerShell 5.1 yet. Because it changes nothing, the worst case is a wrong or empty report, but read it before you run it.

Microsoft pages used: "Fix Wi-Fi connection issues in Windows" (Microsoft Support), "Wireless network connectivity issues troubleshooting" (Microsoft Learn), and the Get-NetAdapterPowerManagement reference (Microsoft Learn). Links are in the repo README.

My repo, with the full script, the tests and a README (free, MIT): https://github.com/fecklesspleb/wifi-drop-diagnostic


AI disclosure: this article and the code were written with AI assistance and reviewed before publishing. Not affiliated with or endorsed by Microsoft.

Top comments (0)