Skip to content
WSL2 Cheat/sheet

Fix a problem

Pick what you are seeing. Each entry starts with a quick diagnosis, then offers the fix that matches what you found. No blind restarts or settings changes.

Networking

Phone cannot reach the dev server#

Your phone on the same Wi-Fi times out opening a dev server or Expo/Metro running in WSL. Find which hop blocks it.

Symptoms#

  • The page works on the PC (localhost:5173) but the phone times out or shows “connection refused”.
  • Expo Go spins on “Opening project…” or reports it could not connect to Metro on port 8081.

A request from the phone has to pass four hops: the Wi-Fi network, the Windows firewall, WSL’s network mode, and the dev server’s bind address. Check them from the inside out.

Diagnose#

  1. Is the server listening on all interfaces, not just loopback?

    WSLBind address of the dev server
    ss -ltn 'sport = :5173'      # 0.0.0.0 or [::] = reachable; 127.0.0.1 = this machine only
  2. Which networking mode is WSL in?

    WSLMirrored mode shows the same IPs as Windows
    wslinfo --networking-mode       # recent WSL; otherwise compare `ip -br addr` with Windows ipconfig
  3. Which IP should the phone use? It is the Windows Wi-Fi adapter’s IPv4 address:

    PowerShellThe PC's LAN address
    ipconfig | Select-String IPv4
  4. Is there a Hyper-V firewall rule for the port?

    PowerShellInbound rules for WSL
    Get-NetFirewallHyperVRule | Where-Object Direction -eq Inbound |
      Select-Object Name, DisplayName, LocalPorts, Action
  5. Is the phone really on the same network? Guest networks and many office or hotel Wi-Fis use client isolation: devices cannot see each other at all.

Fix#

IfStep 1 shows 127.0.0.1

The server only accepts connections from the PC itself. Start it on all interfaces: Vite --host, Next.js -H 0.0.0.0, Expo already does this. Only do this while you need it: the server is then reachable by everyone on the network.

IfStep 2 says nat (not mirrored)

In NAT mode WSL sits behind a private address that the phone cannot reach. Turn on mirrored networking (Windows 11 22H2+). On older Windows, use the portproxy approach in Open a WSL port to devices on your network.

IfStep 4 shows no rule for your port

The Hyper-V firewall blocks inbound traffic to WSL by default. Add a rule for just that port: Open a WSL port to devices on your network.

IfThe phone used a 172.x address, or an address not shown in step 3

The tool printed WSL’s internal address. Use the Windows LAN IP from step 3. For Expo, set REACT_NATIVE_PACKAGER_HOSTNAME=192.168.1.50 (your IP) before npx expo start, or see Open your Expo app on a phone.

IfEverything above looks right, but the network uses client isolation or you are on a VPN

Skip the LAN. npx expo start --tunnel routes Metro through a public tunnel; for a web dev server, see the tunnel alternative in Test a local HTTPS site on your phone.

Verify#

From the phone’s browser, open http://192.168.1.50:5173 (your IP and port). The page loads, and saving a file reloads it on the phone too.

Networking

A port is already in use#

Find what is using port 3000 (or any port), on the Linux side or the Windows side, and free it without restarting everything.

Symptoms#

  • Error: listen EADDRINUSE: address already in use :::3000
  • Port 3000 is in use, trying another one… (Vite, Next.js)
  • bind: address already in use from Docker or a database
  • An An attempt was made to access a socket in a way forbidden by its access permissions error on Windows, even though nothing seems to be listening

Diagnose#

Replace 3000 with your port in every command.

  1. Is something inside WSL listening on it?

    WSLWho listens on port 3000 in Linux
    ss -ltnp 'sport = :3000'
    sudo lsof -iTCP:3000 -sTCP:LISTEN     # shows processes of other users too
  2. Is it a container?

    WSLContainers publishing port 3000
    docker ps --filter publish=3000
  3. Is something on Windows listening on it?

    PowerShellWho listens on port 3000 in Windows
    Get-NetTCPConnection -LocalPort 3000 -State Listen -EA 0 |
      Select-Object LocalAddress, OwningProcess, @{n='Process';e={(Get-Process -Id $_.OwningProcess).ProcessName}}
  4. Nothing found in 1–3? Check whether Windows has reserved the port:

    PowerShellPort ranges reserved by Windows (Hyper-V, WinNAT)
    netsh interface ipv4 show excludedportrange protocol=tcp

Fix#

IfStep 1 shows a process (node, python, …)

It is usually a dev server you started earlier in another terminal or tmux pane. Stop it there with Ctrl+C, or end it by PID (the pid= value from ss):

WSLStop the process
kill <pid>          # polite; use kill -9 <pid> only if it ignores this

IfStep 2 shows a container

Stop that container (docker stop <name>), or change the host side of its port mapping in docker-compose.yml, for example "127.0.0.1:3001:3000".

IfStep 3 shows wslrelay

That is WSL itself. In the default NAT networking mode, wslrelay forwards Windows localhost:3000 to a server running inside WSL. The real owner is a Linux process: go back to step 1 (run it with sudo if ss shows no process name).

IfStep 3 shows another Windows program

With mirrored networking, Windows and WSL share one set of ports, so a Windows app on 3000 blocks WSL from using it (and the other way round). Stop the Windows program, or run your dev server on another port (--port 3001).

IfStep 4 lists a range that contains your port

Windows (Hyper-V / WinNAT) has reserved a block of ports, so nothing may bind there even though no process owns it. The simplest fix is to use a port outside the listed ranges.

Release the reserved ranges by restarting WinNATLast resort

Restarting the WinNAT service usually re-randomises the reserved ranges. It briefly drops NAT networking for WSL, Docker Desktop and Hyper-V VMs, so close them first:

PowerShell · AdminRestart WinNAT (WSL and Docker shut down first)
wsl --shutdown
net stop winnat
net start winnat

Then check excludedportrange again.

Verify#

Run the diagnosis commands again: they return nothing for the port, and your server starts without EADDRINUSE.

Networking

DNS fails after connecting to a VPN#

Names stop resolving inside WSL (but work in Windows) once a corporate VPN connects. Confirm it is DNS, then let WSL use Windows' resolver.

Symptoms#

  • curl: (6) Could not resolve host: github.com
  • Temporary failure in name resolution from apt, git or npm
  • It starts when the VPN connects; Windows apps keep working.

Diagnose#

  1. Is it DNS, or the whole connection? Compare a name with a bare IP:

    WSLName vs IP
    getent hosts github.com            # no output = DNS fails
    curl -sI https://1.1.1.1 | head -1 # a response = the network itself works
  2. Does Windows resolve the same name?

    PowerShellWindows resolver
    Resolve-DnsName github.com
  3. What is WSL using as its resolver?

    WSLCurrent resolver config
    cat /etc/resolv.conf
    grep -iE 'dnsTunneling|autoProxy|networkingMode' /mnt/c/Users/*/.wslconfig 2>/dev/null

Fix#

IfStep 1 resolves nothing but the IP works, and Windows resolves fine

WSL is asking a DNS server the VPN does not allow. DNS tunneling makes WSL resolve through Windows itself, so it follows whatever the VPN configured. It is on by default in recent WSL releases; check your .wslconfig does not turn it off, or set it explicitly:

File%USERPROFILE%.wslconfig
[wsl2]
dnsTunneling=true
autoProxy=true        # also pick up the VPN's proxy settings

Apply with wsl --shutdown from PowerShell, wait about 8 seconds, reopen Ubuntu. Details: .wslconfig → dnsTunneling.

IfThe IP test also fails

It is not DNS: the VPN is blocking or not routing WSL’s traffic at all. Try mirrored networking, which makes WSL traffic look like Windows traffic to most VPN clients. Some VPNs need their own “allow local LAN / WSL” option.

IfDNS tunneling is on and names still fail

Write /etc/resolv.conf by handLast resort

Stop WSL from regenerating the file and point it at your VPN’s DNS server (ask IT, or look at Get-DnsClientServerAddress in PowerShell while connected):

File/etc/wsl.conf (sudo)
[network]
generateResolvConf=false
WSLAfter wsl --shutdown and reopening
sudo rm /etc/resolv.conf      # it is a symlink WSL created
printf 'nameserver 172.16.0.53\nnameserver 1.1.1.1\n' | sudo tee /etc/resolv.conf

Undo: delete the [network] lines from /etc/wsl.conf, sudo rm /etc/resolv.conf, then wsl --shutdown. WSL recreates the file on next start. Note that a hand-written file does not follow the VPN when it changes servers.

Verify#

With the VPN connected, getent hosts github.com prints an address and git fetch works.

Networking

Docker cannot reach my local API#

A container gets "connection refused" or a timeout calling an API or database that runs directly in WSL. Find out where the request goes.

Symptoms#

  • ECONNREFUSED 127.0.0.1:4000 or connect ECONNREFUSED ::1:4000 from inside a container
  • getaddrinfo ENOTFOUND host.docker.internal
  • The same URL works with curl from the WSL shell.

Inside a container, localhost is the container itself, not WSL. The container has to reach the WSL host through another address, and the API has to accept connections on it.

Diagnose#

  1. Which Docker are you using? Docker Engine inside WSL and Docker Desktop behave differently.

    WSLEngine or Desktop
    docker info --format '{{.OperatingSystem}}'   # "Docker Desktop" or your Ubuntu version
  2. What address does the API listen on?

    WSLAPI bind address
    ss -ltn 'sport = :4000'     # 127.0.0.1 = WSL only; 0.0.0.0 or [::] = also containers
  3. What does the container actually resolve and reach?

    WSLTest from inside the container
    docker compose exec app getent hosts host.docker.internal
    docker compose exec app wget -qO- http://host.docker.internal:4000/health   # or curl, if the image has it

Fix#

IfThe app uses localhost in its config

Point it at host.docker.internal instead (for example API_URL=http://host.docker.internal:4000). How to wire that up for both Docker flavours: Connect a container to a service in WSL.

IfStep 3 says host.docker.internal does not resolve (Docker Engine)

Docker Engine on Linux does not define that name by default. Add extra_hosts: ["host.docker.internal:host-gateway"] to the service, as shown in Connect a container to a service in WSL.

IfStep 2 shows 127.0.0.1

The API refuses anything that is not loopback, and container traffic arrives on the Docker bridge address. Start it on all interfaces (--host 0.0.0.0, HOST=0.0.0.0, or the framework’s equivalent). If the API must stay private to the machine, use network_mode: host for the container instead (Docker Engine only).

IfIt resolves and the API listens on 0.0.0.0, but the request times out

A firewall inside WSL is dropping bridge traffic. Check sudo ufw status; if it is active, allow the Docker subnet: sudo ufw allow from 172.16.0.0/12 to any port 4000.

Verify#

The wget (or curl) from step 3 returns your API’s response, and the app in the container stops logging connection errors.

Networking

adb devices shows nothing in WSL#

The Android emulator or a USB phone shows up on Windows but adb inside WSL lists no devices, or fights the Windows adb server.

Symptoms#

  • adb devices in WSL prints List of devices attached and nothing else.
  • adb server version (41) doesn't match this client (39); killing... and the emulator disconnects on Windows.
  • npx expo run:android or react-native run-android cannot find a device.

WSL cannot see USB devices or the Windows emulator directly. The setup in Run Android builds against the Windows emulator makes WSL’s adb talk to the Windows adb server instead.

Diagnose#

  1. Does Windows see the device?

    PowerShellWindows adb
    adb devices
  2. Is WSL pointed at the Windows server, and can it reach it?

    WSLWSL adb settings
    echo $ADB_SERVER_SOCKET          # expect tcp:127.0.0.1:5037
    adb version
    adb devices
  3. Networking mode: reaching 127.0.0.1:5037 on Windows from WSL needs mirrored networking.

Fix#

IfWindows does not list the device either

It is not a WSL problem. Start the emulator from Android Studio, or for a phone enable USB debugging and accept the prompt on the phone.

IfADB_SERVER_SOCKET is empty

WSL started its own adb server, which has no devices. Set the variable as described in Run Android builds against the Windows emulator, open a new terminal, and run adb kill-server once inside WSL so the local server is gone.

IfThe variable is set but adb reports it cannot connect

Without mirrored networking, 127.0.0.1 in WSL is not Windows. Turn on mirrored networking, or follow the NAT-mode alternative in the Android recipe.

IfYou see server version … doesn't match this client

The WSL adb and the Windows adb come from different platform-tools releases and keep restarting each other’s server. Install the same platform-tools version in WSL as on Windows (compare adb version on both sides; download the Linux zip from the Android developer site).

Verify#

adb devices in WSL lists the same device as on Windows, and the emulator stays connected in Android Studio.

Performance & resources

Changes do not trigger hot reload#

You save a file and the dev server or test watcher does not notice. Usually the project lives on /mnt/c, or Linux ran out of file watchers.

Symptoms#

  • Saving a file does not refresh the browser or rebuild.
  • Error: ENOSPC: System limit for number of file watchers reached
  • Watchers work in a small project but not in the monorepo.

Linux tools learn about file changes through inotify. It only works reliably for files on the Linux disk, and the number of watched files is capped.

Diagnose#

  1. Where is the project?

    WSLProject location
    pwd          # starts with /mnt/c/… = on the Windows drive
  2. How many watchers are allowed, and is the error in the dev server output?

    WSLWatcher limits
    cat /proc/sys/fs/inotify/max_user_watches /proc/sys/fs/inotify/max_user_instances
  3. How was the editor opened? The status bar should say WSL: Ubuntu. Opening \\wsl.localhost\… paths directly from a Windows editor bypasses the WSL integration.

Fix#

IfThe path starts with /mnt/c

inotify events from the Windows drive are unreliable, and every file operation there is slow. Move the project to the Linux filesystem:

WSLClone again on the Linux disk
mkdir -p ~/dev && cd ~/dev
git clone <repo-url>

Why this matters for more than watchers: Where files live.

IfYou see ENOSPC, or the limit is 8192–65536 and the repo is large

IfThe editor shows no WSL indicator

Open the project from the WSL shell with code . so the editor runs against Linux: Open a WSL project in your editor.

IfThe project must stay on /mnt/c, or runs in a container with a Windows bind mount

Fall back to pollingAlternative

Polling checks files on a timer instead of waiting for events. It works anywhere but costs CPU, so treat it as a stopgap.

  • Vite: server: { watch: { usePolling: true } } in vite.config.ts
  • Next.js (webpack): WATCHPACK_POLLING=true
  • chokidar-based tools (many): CHOKIDAR_USEPOLLING=true

Verify#

Save a file: the terminal logs a rebuild or HMR update within a second, and the browser updates without a manual refresh.

Performance & resources

Builds and installs are slow#

npm/pnpm install, builds, git status or tests take much longer in WSL than they should. Measure first, then fix the actual bottleneck.

Symptoms#

  • pnpm install or npm ci takes minutes for a project that installs in seconds elsewhere.
  • git status pauses noticeably in a mid-size repo.
  • Builds or test runs are several times slower than on a teammate’s machine.

Diagnose#

  1. Where is the project, and which tools are running?

    WSLLocation and tool paths
    pwd                       # /mnt/c/… = Windows drive
    which node npm pnpm git   # any /mnt/c/… path = a Windows binary
  2. Is Linux short of memory and swapping?

    WSLMemory and swap while the build runs
    free -h
    vmstat 2 5                # non-zero "si"/"so" columns = swapping
  3. Is Windows Defender scanning heavily? Open Task Manager during the build and look for high CPU in Antimalware Service Executable (MsMpEng.exe).

  4. Time the slow step so you can compare after the fix:

    WSLBaseline
    time pnpm install --frozen-lockfile

Fix#

IfThe project is under /mnt/c

This is the cause in most cases: every file operation crosses the Windows/Linux boundary. Move the project into ~/dev (see Where files live). Expect 5–20× faster installs.

Ifwhich shows a /mnt/c/… path

A Windows Node or Git is running on Linux files. Fix the PATH: node or npm is the Windows one.

IfSwap is in use during builds

WSL needs more memory, or something else is holding it. Check what uses it (WSL uses too much memory), then raise the limit in Cap WSL memory and CPU.

IfDefender is busy during installs

Excluding the WSL virtual disk from real-time scanning gives a noticeable I/O win, at the cost of not scanning that disk: Speed up installs with Defender exclusions.

Verify#

Repeat the timed command from step 4. Note both times; if it is still slow after moving the project, work through the remaining findings one by one so you know which change helped.

Performance & resources

Too many open files#

A dev server, test runner or database fails with EMFILE because it hit the per-process open file limit.

Symptoms#

  • Error: EMFILE: too many open files, watch or …, open '…'
  • accept: too many open files from a server
  • Different from ENOSPC … file watchers (that is hot reload).

Diagnose#

  1. What limits does your shell have?

    WSLSoft and hard limits
    ulimit -Sn; ulimit -Hn
  2. For a service started by systemd (Docker, databases), check the service’s own limit:

    WSLLimit of a running service
    systemctl show docker -p LimitNOFILE
  3. How many files does the failing process hold? While it runs:

    WSLOpen files of a process
    ls /proc/<pid>/fd | wc -l

Fix#

IfThe soft limit is 1024 (Ubuntu's default)

Raise the limits for login shells and systemd services: Raise open-file limits.

IfThe limit is already high and the process still runs out

The process is leaking descriptors, or watching far more than it needs (for example node_modules). Check the tool’s ignore settings for watchers before raising limits further.

Verify#

Open a new terminal: ulimit -Sn shows the new value, and the failing command runs without EMFILE.

Performance & resources

WSL uses too much memory#

Task Manager shows VmmemWSL holding many gigabytes, and Windows gets slow. Find out whether it is real usage or Linux cache.

Symptoms#

  • Task Manager shows VmmemWSL (or Vmmem) using 8, 16 GB or more.
  • Windows apps slow down or start swapping while WSL is open.
  • Memory does not go down after you stop your dev servers.

Diagnose#

  1. How much of WSL’s memory is actually used by processes, and how much is cache?

    WSLUsed vs cache
    free -h     # "used" = processes; "buff/cache" = file cache Linux can drop
  2. Which processes use the most?

    WSLTop memory users
    ps aux --sort=-%mem | head -n 8
    docker stats --no-stream 2>/dev/null
  3. Is there a cap in .wslconfig? Without one, WSL may grow to half your RAM (more on some versions).

    PowerShellCurrent WSL config
    Get-Content $env:USERPROFILE\.wslconfig -EA 0

Fix#

IfMost of it is buff/cache

That is file cache from builds and installs. Linux keeps it because it is free to drop, but WSL is slow to hand it back to Windows. Turn on autoMemoryReclaim so idle cache returns to Windows: Cap WSL memory and CPU. For an immediate release, run wsl --shutdown in PowerShell when you are done working.

IfA process or container uses most of it

Stop what you do not need (docker compose down, old dev servers in other terminals or tmux sessions). Databases and Elasticsearch-style services often have their own memory settings worth lowering in development.

IfThere is no memory= cap

Set one that leaves Windows enough room, typically 50–75% of RAM: Cap WSL memory and CPU.

Verify#

After the change (and a wsl --shutdown if you edited .wslconfig), free -h in WSL shows the new total, and VmmemWSL in Task Manager drops back after builds finish.

Performance & resources

Windows disk space is running out#

C: is filling up and WSL is the suspect. Measure the virtual disk and what is inside it before deleting anything.

Symptoms#

  • Windows warns that C: is low on space.
  • The WSL virtual disk (ext4.vhdx) is much larger than what df shows as used inside Linux.
  • Docker images and build cache keep piling up.

Your whole distro lives in one file, ext4.vhdx. It grows as you write, and by default does not shrink when you delete files inside Linux.

Diagnose#

  1. How big is the virtual disk on Windows?

    PowerShellFind ext4.vhdx files and their size
    Get-ChildItem "$env:LOCALAPPDATA\wsl", "$env:LOCALAPPDATA\Packages\CanonicalGroupLimited.*" -Recurse -Filter ext4.vhdx -EA 0 |
      Select-Object FullName, @{n='GB';e={[math]::Round($_.Length/1GB,1)}}
  2. How much does Linux actually use?

    WSLUsed space inside the distro
    df -h /
  3. Where is it going?

    WSLBiggest folders and Docker usage
    sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -n 8
    du -h --max-depth=1 ~ 2>/dev/null | sort -h | tail -n 8
    docker system df

Fix#

IfThe vhdx is much bigger than df's used space

The disk is not giving deleted space back. Turn on sparse mode and TRIM, or compact it once: Shrink the WSL virtual disk.

IfDocker's images, volumes or build cache are large

Reclaim what is unused. docker system prune removes stopped containers, unused networks, dangling images and build cache; it asks before doing so. Add -a to also remove unused images (they re-download on next use). Volumes are kept unless you add --volumes, which deletes database data. Cap logs and build cache so it does not return: Install Docker in WSL.

IfCaches in your home folder are large (~/.cache, ~/.local/share/pnpm, ~/.npm)

These are safe to clean; tools re-download what they need. pnpm store prune, npm cache clean --force, and rm -rf ~/.cache/* for the rest.

Verify#

Repeat step 1 after shrinking: the vhdx size is close to df’s used space, and Windows shows the space as free.

WSL itself

Not sure what is wrong? Run a health check#

A two-minute check of WSL, systemd, networking and Docker after an update or a config change. Each result links to the matching fix.

Symptoms#

Something feels off after an update, a reboot or a change to .wslconfig / wsl.conf, and you do not yet know what. Start here; every check is read-only.

Diagnose#

  1. WSL itself, from PowerShell:

    PowerShellVersion and distro state
    wsl --version          # WSL and kernel versions
    wsl -l -v              # distros, state, and VERSION 2
  2. Inside Ubuntu:

    WSLHealth check
    free -h && nproc                  # matches memory= and processors= in .wslconfig?
    systemctl is-system-running       # "running", not "degraded"
    systemctl --failed
    ip -br addr                       # mirrored mode: same IPs as Windows
    getent hosts github.com           # DNS works
    docker ps                         # works without sudo (if you use Docker)

Fix#

IfMemory or CPU count does not match your .wslconfig

The file was not applied: .wslconfig changes do nothing.

Ifsystemctl is-system-running says degraded

Ifsystemctl says System has not been booted with systemd

systemd is off for this distro. Turn it on with [boot] systemd=true: wsl.conf → systemd.

Ifgetent hosts prints nothing

DNS fails after connecting to a VPN covers DNS failures with or without a VPN.

Ifdocker ps says permission denied

Your user is not in the docker group yet, or the terminal predates it. See the steps in Install Docker in WSL, then open a new terminal.

Ifwsl -l -v shows VERSION 1

Convert it: wsl --set-version Ubuntu 2 in PowerShell (it can take a while for a big distro).

Verify#

Every line above gives the expected result. Keep this page bookmarked for the next WSL update.

WSL itself

.wslconfig changes do nothing#

You edited %USERPROFILE%\.wslconfig but memory, networking or other settings did not change. Check the file name, the restart, and the key.

Symptoms#

  • free -h still shows the old memory size after you changed memory=.
  • Mirrored networking, DNS tunneling or another key seems to have no effect.

Diagnose#

  1. Is the file really named .wslconfig, in your Windows user folder?

    PowerShellExact file name and location
    Get-ChildItem $env:USERPROFILE -Force -Filter '.wslconfig*' | Select-Object Name, Length, LastWriteTime
  2. Did WSL fully stop since you edited it? Closing terminal tabs is not enough.

    PowerShellRunning distros
    wsl -l --running
  3. Does WSL accept the key? Open WSL Settings from the Start menu: it is a GUI over this file and only shows keys your WSL version supports. Unknown or misspelled keys are ignored (recent versions print a warning when WSL starts).

Fix#

IfThe name is .wslconfig.txt (Notepad added it) or the file is elsewhere

Rename it: Rename-Item $env:USERPROFILE\.wslconfig.txt .wslconfig. It must be in %USERPROFILE%, not inside the distro and not in /etc.

IfDistros are still running

Shut WSL down, wait about 8 seconds so the VM really stops, then open Ubuntu again:

PowerShellApply .wslconfig
wsl --shutdown

IfThe key is under the wrong section, or WSL Settings does not show it

Keys like autoMemoryReclaim and sparseVhd have moved between [wsl2] and [experimental] across releases. Check the key in .wslconfig reference and update WSL with wsl --update.

Verify#

After reopening Ubuntu, the setting shows up: free -h reports the new memory total, nproc the new CPU count, and mirrored mode shows the same IPs as Windows in ip -br addr.

WSL itself

systemctl says degraded#

systemctl is-system-running reports degraded. Find the failed unit; on WSL it is usually systemd-binfmt, which is safe to mask.

Symptoms#

systemctl is-system-running prints degraded instead of running. Services may still work, but tools that wait for a healthy system can hang or complain.

Diagnose#

WSLWhich units failed, and why
systemctl --failed
journalctl -b -u systemd-binfmt.service --no-pager | tail -n 5   # if it is listed

Fix#

Ifsystemd-binfmt.service failed

It tries to write to WSL’s read-only binfmt_misc, which WSL manages itself. Masking it is safe: running Windows .exe files from Linux keeps working.

WSLStop systemd-binfmt from starting
sudo systemctl mask systemd-binfmt.service

Undo: sudo systemctl unmask systemd-binfmt.service.

IfAnother unit failed

Read its log with journalctl -b -u <unit> --no-pager. Units for hardware that does not exist in WSL (for example some *-udev* or snap services) can usually be disabled with sudo systemctl disable <unit>; anything you installed yourself needs its own fix.

Verify#

Run wsl --shutdown in PowerShell, reopen Ubuntu, wait a few seconds, then systemctl is-system-running prints running.

WSL itself

Clock is wrong after sleep#

After the PC sleeps, WSL's clock lags behind Windows, causing TLS errors, failed apt updates or expired tokens.

Symptoms#

  • Release file … is not valid yet from apt update
  • TLS or certificate errors such as certificate is not yet valid
  • Signed URLs, OTP codes or tokens are rejected as expired

Diagnose#

Compare the clocks:

WSLLinux time vs Windows time
date
powershell.exe -NoProfile -Command Get-Date 2>/dev/null || /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -Command Get-Date

Fix#

IfLinux is behind Windows by more than a few seconds

Resync from the host clock, or restart the time sync service:

WSLResync the clock
sudo hwclock -s || sudo systemctl restart systemd-timesyncd

If neither helps, wsl --shutdown in PowerShell always fixes it, because the VM starts with the Windows time. Recent WSL versions resync after sleep on their own; wsl --update if it keeps happening.

Verify#

date in WSL matches Windows, and sudo apt update runs without “not valid yet” errors.

WSL itself

GPU or CUDA not found#

nvidia-smi or CUDA does not see the GPU inside WSL. WSL uses the Windows driver; a Linux GPU driver breaks it.

Symptoms#

  • nvidia-smi: command not found, or it reports no devices.
  • torch.cuda.is_available() returns False.

Diagnose#

WSLDoes WSL see the Windows driver?
ls /usr/lib/wsl/lib/ | grep -i -E 'cuda|nvidia'
/usr/lib/wsl/lib/nvidia-smi
dpkg -l | grep -i -E 'nvidia-driver|nvidia-dkms'     # should print nothing

Fix#

If/usr/lib/wsl/lib has no NVIDIA files

Install or update the NVIDIA driver on Windows (the normal Game Ready or Studio driver includes WSL support), then wsl --shutdown and reopen.

Ifdpkg lists a Linux NVIDIA driver

A Linux driver overrides the WSL passthrough. Remove it: sudo apt purge 'nvidia-driver-*' 'nvidia-dkms-*', then wsl --shutdown. For CUDA development, install only the WSL-Ubuntu CUDA toolkit package from NVIDIA, which contains no driver.

Verify#

nvidia-smi (or /usr/lib/wsl/lib/nvidia-smi) lists your GPU, and your framework reports CUDA as available.

Shell & tools

code, explorer.exe or powershell.exe not found#

Windows programs stopped working from the WSL shell. Usually appendWindowsPath=false removed them from PATH, which is fixable without undoing it.

Symptoms#

  • code: command not found or powershell.exe: command not found
  • explorer.exe . stopped opening Explorer

Diagnose#

WSLIs interop on, and is the Windows PATH appended?
grep -A3 '^\[interop\]' /etc/wsl.conf
ls /mnt/c/Windows/System32/cmd.exe
cat /proc/sys/fs/binfmt_misc/WSLInterop 2>/dev/null | head -n 1   # "enabled" = Windows .exe files can run

Fix#

IfappendWindowsPath=false is set

That is expected: the setting keeps the Windows PATH out of Linux (faster shell, no Windows node sneaking in). Add back only the few Windows commands you use: Call Windows apps from WSL without the Windows PATH. For a one-off, use the full path, for example /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe.

Ifenabled=false under [interop], or WSLInterop is not enabled

Running Windows programs is switched off for this distro. Set enabled=true in wsl.conf → interop, then wsl --shutdown.

IfInterop is on and the Windows PATH is appended, but code is still missing

VS Code was installed after the distro started, or for a different Windows user. Run wsl --shutdown, reopen Ubuntu, and check that code works in a Windows terminal first.

Verify#

code --version and explorer.exe . work from the WSL shell.

Shell & tools

node or npm is the Windows one#

which node points into /mnt/c, so a Windows Node.js runs against Linux files, with slow installs and broken native modules.

Symptoms#

  • which node prints /mnt/c/Program Files/nodejs/node.
  • Installs are very slow, or native modules fail with invalid ELF header.
  • npm scripts fail with '\r': command not found or Windows-style paths.

Diagnose#

WSLWhich binaries win on PATH
which -a node npm npx pnpm
echo "$PATH" | tr ':' '\n' | grep -n -E 'mise|nvm|/mnt/c'

Fix#

IfThere is no Linux node at all

Install a Linux runtime: Install runtimes with mise. Make sure the activation line is in your shell’s rc file and open a new terminal.

IfA Linux node exists but /mnt/c comes first

mise (or nvm) is activated after the Windows PATH was added, or not at all in this shell. Check that ~/.bashrc (or ~/.zshrc) runs mise activate, then open a new terminal. To keep Windows tools out of PATH for good, set appendWindowsPath=false: Call Windows apps from WSL without the Windows PATH.

Ifnode_modules was installed by the Windows node

Native modules were built for Windows. After fixing PATH, reinstall: rm -rf node_modules && pnpm install (or npm ci).

Verify#

which node prints a path under ~/.local/share/mise (or ~/.nvm), and node -p process.platform prints linux.

Shell & tools

sudo -E fails or is ignored#

Newer Ubuntu releases ship sudo-rs, which does not support every option of classic sudo. Pass the variables explicitly instead.

Symptoms#

  • sudo -E some-command reports an unknown or unsupported option.
  • Variables you exported are missing inside the command run with sudo.

Diagnose#

WSLWhich sudo is this?
sudo --version | head -n 1      # "sudo-rs …" or "Sudo version 1.9…"

Fix#

IfIt prints sudo-rs

Pass the variables explicitly; this works with both implementations:

WSLSet variables for one sudo command
sudo env HTTPS_PROXY="$HTTPS_PROXY" FOO=bar some-command

If a script you cannot change depends on classic sudo behaviour, Ubuntu still ships it as sudo.ws (sudo.ws -E …); see Ubuntu’s release notes for how to switch the default.

IfIt prints classic Sudo version

-E is supported, but the sudoers policy may still drop some variables (env_reset). Use the sudo env … form above, which is explicit and survives policy changes.

Verify#

sudo env FOO=bar sh -c 'echo $FOO' prints bar.

Shell & tools

Every file shows as changed#

git status lists files you never touched, or scripts fail with $'\r' errors. Something wrote Windows line endings (CRLF) or changed file modes.

Symptoms#

  • git status shows many modified files with no visible change.
  • git diff shows ^M at line ends, or old mode 100644 / new mode 100755.
  • Shell scripts fail with $'\r': command not found or /bin/bash^M: bad interpreter.

Diagnose#

WSLWhat actually changed
git diff --stat | tail -n 3
git diff | grep -c $'\r'             # > 0 = CRLF in the changes
git config --show-origin --get-all core.autocrlf
git config --get core.filemode
pwd                                  # /mnt/c/… = repo on the Windows drive

Fix#

IfThe diff contains \r (CRLF)

Something on Windows (an editor, Windows Git) touched the files. Keep LF in the repo and in your working copy:

WSLKeep LF everywhere
git config --global core.autocrlf input
printf '* text=auto eol=lf\n' >> .gitattributes
git add --renormalize .

Commit .gitattributes so the rule travels with the repo.

IfOnly file modes changed and the repo is on /mnt/c

The Windows drive emulates Linux permissions. Move the repo to ~/dev (Where files live), or, if it must stay, git config core.filemode false in that repo.

Verify#

git status is clean, and scripts run without \r errors.