Troubleshooting · Network

Port in use but no process is listed on Windows

netstat shows nothing on the port, yet your program still cannot bind to it. Work through the causes one check at a time: reserved port ranges, TIME_WAIT, PID 4, IPv6, Docker and WSL forwarding, permissions and the firewall.

Reading time
8 min
Updated
Applies to
Windows 10, Windows 11
Privileges
Standard user to diagnose; administrator for the fixes
Difficulty
Intermediate

Your program refuses to start because its port is taken, so you run netstat -ano | findstr ":8080" and get nothing back. No listener, no PID, nothing to stop, and the bind fails again on the next try. This happens more on Windows than on other systems because several things besides a listening process can make a port unavailable. The checks below run in the order they are worth doing, one command each. The examples use 8080; substitute your port.

Start with the exact error

The error code narrows the search before you run anything.

Message Code What it usually means
Only one usage of each socket address is normally permitted / EADDRINUSE 10048 A socket is bound to that address and port, or was until very recently
An attempt was made to access a socket in a way forbidden by its access permissions / EACCES 10013 The port is inside a reserved range
Connection refused / ECONNREFUSED 10061 Not a bind problem: nothing was listening when the client connected
Connection timed out 10060 Not a bind problem: a firewall or route is dropping the traffic

Only the first two are about binding. For the third or fourth, nothing is holding your port; see the firewall section near the end.

Check 1: look at the whole table

Three narrow searches miss real listeners:

  • Filtering on one address. findstr "127.0.0.1:8080" misses 0.0.0.0:8080 and [::]:8080. Filter on the port alone.
  • Ignoring IPv6. A listener on [::]:8080 is dual-stack by default on Windows and claims the IPv4 port as well. If you only looked at IPv4 rows you will not see it.
  • Ignoring UDP. Get-NetTCPConnection does not return UDP sockets. If the program binds UDP, ask Get-NetUDPEndpoint.
Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue |
  Select-Object LocalAddress, LocalPort, State, OwningProcess
Get-NetUDPEndpoint -LocalPort 8080 -ErrorAction SilentlyContinue |
  Select-Object LocalAddress, LocalPort, OwningProcess

Leaving out -State Listen matters here: it keeps TimeWait, CloseWait and Bound rows, which the next checks need. Run it immediately after the failure, too. A server under a file watcher restarts in under a second, and a socket that existed when the bind failed can be gone by the time you look.

Check 2: reserved port ranges

Hyper-V, WSL 2, Docker Desktop and the Windows NAT driver (winnat) reserve blocks of ports for their own use. A reserved port has no listener and no PID, but a bind to it fails, usually with error 10013. On developer machines this is the most common cause of “in use but nobody is using it”.

netsh interface ipv4 show excludedportrange protocol=tcp
Protocol tcp Port Exclusion Ranges

Start Port    End Port
----------    --------
      1061        1160
      5000        5099
      8032        8131
     50000       50059     *

* - Administered port exclusions.

If your port sits inside a range, you have found the cause. Rows without * are dynamic reservations that Hyper-V and friends take when they start, which is why the blocked port changes between reboots and why the same project works on a colleague’s machine. Rows with * are persistent exclusions that someone added on purpose. Repeat the command with protocol=udp or ipv6 if those apply to you.

Three fixes, least invasive first:

  1. Use a port outside every listed range. For a development server this is usually enough.
  2. Restart the NAT service so the dynamic ranges are reallocated (see Restarting winnat). Your port may come back, or a different one may be taken instead.
  3. Reserve the port for yourself permanently. From an elevated terminal, while the port is not inside a current range (stop winnat first if it is):

Warning

This changes a persistent system setting that survives reboots. Note the exact command so you can reverse it later with netsh interface ipv4 delete excludedportrange protocol=tcp startport=8080 numberofports=1 store=persistent.

netsh interface ipv4 add excludedportrange protocol=tcp startport=8080 numberofports=1 store=persistent

This adds an administered exclusion, the * kind. Hyper-V and WSL will no longer allocate it, and your own program can still bind it.

The dynamic reservations are carved out of the dynamic port range, so check where that range starts:

netsh interface ipv4 show dynamicport tcp

The default on Windows 10 and 11 is a start port of 49152 with 16384 ports. Some machines end up with a range starting at 1024, and then Hyper-V reserves blocks exactly where developers put their servers. Reset it from an elevated terminal and reboot:

Warning

This changes a persistent system setting that takes effect after a restart. Note the current values from netsh interface ipv4 show dynamicport tcp first so you can restore them.

netsh interface ipv4 set dynamicport tcp start=49152 num=16384

Check 3: TIME_WAIT

When a connection closes, the side that closed first keeps the socket in TIME_WAIT for a while, typically up to two minutes, so that late packets do not land in a new connection. These rows show PID 0: they belong to nobody.

netstat -ano | findstr ":8080"
  TCP    127.0.0.1:8080         127.0.0.1:51202        TIME_WAIT       0
  TCP    127.0.0.1:8080         127.0.0.1:51203        TIME_WAIT       0

On Windows, TIME_WAIT entries left over from a previous server normally do not stop a new listener from binding. The exception is a server that binds with the exclusive-use option (SO_EXCLUSIVEADDRUSE): it can be refused until the old connections have aged out. Waiting two minutes resolves it; so does starting the server on another port.

The client-side version looks similar: a program that opens thousands of short connections can exhaust the dynamic port range and fail with the same 10048 error on outgoing connections. Counting the rows is the quick tell:

netstat -ano | find /c "TIME_WAIT"

Tens of thousands means the client should reuse connections; a few hundred is normal.

Check 4: PID 4 and HTTP.sys

When the owner is PID 4, the port belongs to the Windows kernel, which tasklist lists as System. Two kernel components own ports this way. HTTP.sys listens on behalf of IIS, WinRM (5985 and 5986), Windows Admin Center and any application built on HttpListener; the SMB server owns 445 and 139. The program that really has the port is whichever one registered a URL with HTTP.sys.

netsh http show servicestate

This lists the request queues HTTP.sys knows about, with the process ID behind each one and the URLs it registered (http://+:8080/, for example). Use the PID to identify the application, then stop it the normal way. For WinRM:

Warning

This needs an elevated PowerShell and stops remote management (PowerShell remoting, Windows Admin Center and similar tools) until the service is started again with Start-Service -Name WinRM.

Stop-Service -Name WinRM

netsh http show urlacl lists URL reservations, which are permissions to listen rather than listeners; a reservation on its own does not block your port.

Check 5: permissions

Neither netstat -ano nor Get-NetTCPConnection needs elevation to show PIDs, so a missing row is not a permission problem. Permissions bite one step later:

  • netstat -b prints “The requested operation requires elevation”.
  • Get-Process -Id returns the name but a blank Path, and tasklist shows N/A in some columns, when the process runs under another account, as a service, or as a protected process (antimalware, for example).
  • taskkill answers “Access is denied” for those processes.

Open an elevated terminal and repeat the command. Protected processes stay closed even then; stopping their service is the supported route.

Check 6: Docker Desktop and WSL 2

Ports published from containers and ports opened inside a WSL 2 distribution are forwarded to Windows by a relay process. The Windows side sees the relay, not the real server:

Owning process What it is
com.docker.backend.exe Docker Desktop publishing a container port (-p 8080:80)
wslrelay.exe or wslhost.exe WSL 2 forwarding a port that a Linux process opened, so localhost:8080 works from Windows

To find the real owner, ask the other side:

docker ps --format "table {{.Names}}\t{{.Ports}}"
wsl -- ss -ltnp

ss -ltnp lists listening TCP sockets inside the distribution with the process name and PID (sudo is needed to see processes owned by other users). Stop the container with docker stop <name>, stop the Linux process, or as a last resort run wsl --shutdown.

Check 7: the firewall is not “in use”

Windows Defender Firewall decides whether packets reach a listener; it never prevents a bind. If the server starts but clients cannot connect, test from the same machine first:

Test-NetConnection -ComputerName localhost -Port 8080

TcpTestSucceeded : True means the server is listening and the problem lies between the remote client and this PC: a firewall rule, the network profile (public versus private), or a listener bound to 127.0.0.1 only. The rules that mention the port:

Get-NetFirewallPortFilter -Protocol TCP |
  Where-Object LocalPort -eq 8080 |
  Get-NetFirewallRule |
  Select-Object DisplayName, Enabled, Direction, Action, Profile

Restarting winnat

The Windows NAT service hands out the dynamic reserved ranges. Restarting it releases them and allocates fresh ones, which often frees a port that Hyper-V had taken.

Warning

Stopping winnat interrupts the NAT networking that Docker Desktop, WSL 2 and Hyper-V’s Default Switch rely on. Running containers lose their published ports and virtual machines can lose connectivity until the service is back, and the new reservations may land on a different port you were using. Do this between tasks, not during a demo, and prefer a persistent exclusion for the ports you depend on.

From an elevated terminal:

net stop winnat
net start winnat

If the port is still blocked after the restart, list the exclusions again: the dynamic range may simply have moved onto it, or it may be an administered exclusion, which net stop does not touch.

Where PortWho fits, and where it does not

PortWho is Utility Forge’s port inspector. It is currently in development: the read-only engine, the command line and a desktop application are implemented and tested internally, and there is no public download yet. Running portwho 8080 shows every listening TCP socket and bound UDP socket on the port with its PID, process name, executable path, hosted Windows services and Authenticode signature. On this page’s problem it helps with Check 1, because IPv4, IPv6, TCP and UDP appear in one listing; with Check 4, because a PID 4 row is reported with a system status instead of an error; and with Check 5, because a process it cannot open is reported as accessDenied rather than left out.

It does not solve the rest. It lists listening TCP sockets and bound UDP sockets only, so TIME_WAIT rows never appear. It does not read the excluded port ranges yet, so a reserved port looks like nothing is listening. And it never changes anything, so stopping a process or restarting winnat remains your job, done with the commands above. Those limits are deliberate; the PortWho page tracks its status and the security page explains what the tool is and is not allowed to do.

FAQ

Frequently asked

Why did the port work after a reboot and then fail again a few days later?

The dynamic reservations that Hyper-V, WSL 2 and Docker Desktop take through the Windows NAT service are allocated when the service starts and can land on different ports each time. A port that was free on Monday can be inside a reserved range on Thursday. A persistent exclusion for the ports you rely on, added with netsh, stops the lottery.

The error says permission denied. Do I need to run my server as administrator?

No. Windows has no rule that ports below 1024 need elevation, so error 10013 (EACCES) on a bind almost always means the port is inside a reserved range. Running the server elevated changes nothing about the reservation and gives the process more rights than it needs. Check the excluded port ranges first.

Can I shorten how long sockets stay in TIME_WAIT?

Yes, the TcpTimedWaitDelay value under HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters accepts 30 to 300 seconds and takes effect after a reboot. On a workstation it is rarely worth it. If a client exhausts ports with short connections, fix the client to reuse connections; if a server is refused because of TIME_WAIT rows, waiting a couple of minutes or changing the port is simpler.

Is PID 0 a process I can kill?

No. PID 0 on a TIME_WAIT row means no process owns the socket any more; the TCP stack is holding the entry for a short time and will drop it by itself. There is nothing to terminate.