If a dev tool on Windows has ever failed on you with this line:
An attempt was made to access a socket in a way forbidden by its access permissions. (os error 10013)
...your first instinct is probably the firewall, or antivirus, or "I need to run this as admin". I've gone down that road. Turned Defender off, opened an elevated terminal, got the exact same error. Most of the time it's something more boring: Windows has already given that port away, and nothing in the error message says so.
Two errors that look alike and mean opposite things
If you spend any time in Windows socket logs, you'll keep meeting two of them.
-
10048(WSAEADDRINUSE) means something is already listening on that port.netstat -ano | findstr :47311gives you the PID, and killing that process fixes it. -
10013(WSAEACCES) means you aren't allowed to bind there at all. Nothing is listening. The firewall isn't involved, and running as admin usually changes nothing, because the port sits inside a range Windows has reserved.
On any machine running Hyper-V, WSL2 or Docker Desktop, the Host Network Service (WinNAT) books blocks of TCP ports for itself when it starts. Those blocks aren't occupied, they're reserved. A tool that grabs a random port for a local listener, like an OAuth callback, a preview server or a test runner, can land inside one. And since the port gets picked at random each run, the failure comes and goes. That's the part that makes it so annoying to chase.
Step 1: look at the reservations
No admin needed for this one:
netsh int ipv4 show excludedportrange protocol=tcp
You get a table of start ports and the number of ports in each block. On a box with WSL2 or Docker Desktop I usually see a few blocks of 100 somewhere up in the 49000-51000 region, but which bases you get depends on the machine and can shift between boots. Run it fresh instead of trusting output you saved last month.
If you want the ephemeral range as well:
netsh int ipv4 show dynamicport tcp
Then check whether your port falls inside one of the blocks. Everything from startport up to startport + numberofports - 1 is covered.
Step 2: move the port, if you're allowed to
This is the fix that actually stuck for the person in the thread I was reading (github/copilot-cli#4463). Copilot CLI picks its OAuth callback port automatically, and the listener blew up with 10013 before the browser even opened, which is a fun way to spend an afternoon. He listed the excluded ranges, pinned the port to 47000, restarted, and the auth flow completed.
Most tools that do a loopback OAuth flow have a setting for this somewhere. For Copilot CLI it's the auth.redirectPort key in the config:
{
"auth": { "redirectPort": 47000 }
}
For a plain dev server it's whatever your framework gives you: --port, PORT=, a listen() argument. Pick something clear of every block, re-run show excludedportrange, and confirm nothing covers it.
One thing worth saying out loud. If the tool worked yesterday and fails today, that doesn't mean the problem fixed itself. It picked a different port. It'll be back.
Step 3: if the port has to stay
Two options here, one temporary and one that sticks. Both need an elevated prompt.
The temporary one releases the NAT service's holds around your bind:
net stop winnat
# start your service, or finish the bind, here
net start winnat
Restart-Service winnat does it in a single step if you only need the reservations dropped and re-taken. While winnat is stopped, outbound NAT and port forwarding for WSL2, Hyper-V VMs and Docker Desktop containers degrade, so don't do this on a machine that's actively serving containers. The reservations also come back the next time the service starts, reboot included, and on newer builds the Host Network Service may re-take them almost immediately, in which case this buys you nothing.
The durable option is to make the reservation yours instead:
netsh interface ipv4 add excludedportrange protocol=tcp startport=47311 numberofports=1 store=persistent
Windows now keeps that port out of dynamic allocation, so nothing else claims it and your explicit bind keeps working across restarts. store=persistent is the part that survives a reboot. To undo it later:
netsh interface ipv4 delete excludedportrange protocol=tcp startport=47311 numberofports=1 store=persistent
You are now the owner of a reserved port. That's the whole point, but it's worth remembering you did it, for the day months from now when something else can't have that port.
If the excluded list is empty and you still get 10013
Then it isn't the NAT service. The usual suspects at that point are endpoint security and VPN filter drivers. Some EDR and AV products, plus a few corporate VPN clients, install a filter that refuses binds for specific programs, and the error surfaces as the same 10013. The tell is a binary that fails on one machine and works on another with the same port, along with a block event in the security product's own log. I haven't hit that one often, so I'd read the vendor log before guessing at it.
The short version
I treated 10013 as a permissions problem for longer than I'd like to admit before it occurred to me to just look at the reserved ranges first. It's a two second command, and it tells you straight away whether you're chasing a port collision or something else entirely. Run it before you touch the firewall:
netsh int ipv4 show excludedportrange protocol=tcp
Topic came from a stuck-user thread over on the Copilot CLI repo. The port-pinning fix there is the reporter's, not mine, and it's the one that got him unblocked.
Top comments (0)