Skip to content

bind() takes ~10 ms for every socket family under WSL2, independent of networking mode #41470

Closed

Description

Windows Version

Microsoft Windows [Version 10.0.26200.9106]

WSL Version

2.7.10.0

Are you using WSL 1 or WSL 2?

  • WSL 2
  • WSL 1

Kernel Version

6.18.33.2-2

Distro Version

Ubuntu 24.04 (Noble Numbat)

Other Software

openvpn3-linux v26-3 and v27.1 (OpenVPN core 3.11.5 / 3.11.7) — affected, but only
as a symptom. The benchmark below reproduces the underlying behaviour with nothing
installed beyond Python 3.

Repro Steps

bind() takes ~10 ms on every socket family under WSL2, while socket() creation
is free. This is not specific to networking mode — a private (non-mirrored) network
namespace shows the same cost.

  1. .wslconfig:
[wsl2]
networkingMode=mirrored
dnsTunneling=true
firewall=false
autoProxy=true

[experimental]
hostAddressLoopback=true
  1. Save and run this benchmark. socket() is timed separately as a control:
import socket, time, statistics, os
N = 200
def bench(label, make, act):
    ts = []
    for _ in range(N):
        s = make()
        t0 = time.perf_counter(); act(s); t1 = time.perf_counter()
        ts.append((t1 - t0) * 1000); s.close()
    ts.sort()
    print(f"{label:36s} median {statistics.median(ts):7.3f} ms  min {ts[0]:6.3f}  max {ts[-1]:7.3f}")

bench("AF_NETLINK bind()",
      lambda: socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE),
      lambda s: s.bind((0, 0)))
bench("AF_NETLINK socket() only (control)",
      lambda: socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE),
      lambda s: None)
bench("AF_UNIX bind() (abstract)",
      lambda: socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM),
      lambda s: s.bind("\0b%d_%d" % (os.getpid(), time.time_ns())))
bench("AF_INET/UDP bind()",
      lambda: socket.socket(socket.AF_INET, socket.SOCK_DGRAM),
      lambda s: s.bind(("127.0.0.1", 0)))
  1. To separate mirrored-mode cost from baseline cost, run the same script again
    inside a private network namespace, which is not mirrored:
sudo unshare -n -- sh -c "ip link set lo up; exec python3 bench.py"

Expected Behavior

bind() on AF_UNIX and AF_NETLINK involves no network stack coordination and
should complete in microseconds, as it does on a conventional Linux kernel.
AF_INET bind() to an ephemeral port on loopback should likewise be
inexpensive.

Actual Behavior

                            host netns      private netns
AF_NETLINK bind()            10.098 ms       10.092 ms     <- unchanged
AF_UNIX bind()               10.150 ms       10.495 ms     <- unchanged
AF_INET/UDP bind()           21.141 ms       10.500 ms     <- halved
socket() only (control)       0.000 ms        0.000 ms

Two independent effects:

  1. A baseline ~10 ms bind() cost on every socket family, identical inside a
    private namespace. Not related to mirrored networking. This is the one with
    real impact.
  2. Mirrored mode adds a second ~10 ms increment, but only to AF_INET
    (21 ms vs 10.5 ms). Plausibly port coordination with the Windows host —
    AF_UNIX and AF_NETLINK have no ports and are unaffected.

socket() measuring ~0 ms rules out general syscall overhead.

Correction (see comment below). I originally wrote that the ~10 ms figure
looked like a scheduler tick. That is wrong — this kernel is CONFIG_HZ=250
(a 4 ms tick), and 10 ms is not a multiple of 4 ms. A 400-sample histogram is
sharply unimodal at 10–12 ms for AF_NETLINK (97% within 8–12 ms) and at
20–22 ms for AF_INET (90%), i.e. almost exactly 2×. That points at one and
two instances of a fixed ~10 ms synchronous cost rather than anything
timer-aligned. Confirmed independently of Python via strace -T on
openvpn3-service-netcfg (C++): 901 bind() calls, mean 9.1 ms, max 10.8 ms.

Real-world impact. openvpn3-linux cannot establish a tunnel at all. Its
openvpn3-service-netcfg opens a fresh AF_NETLINK socket per configuration
operation; with a split-tunnel profile pushing ~900 routes (a commonplace
corporate configuration) that is ~900 × ~10 ms ≈ 9 s, against a fixed 5 s D-Bus
timeout on its Establish() call:

901 bind() calls    mean 9.1 ms   max 10.8 ms   total 8.183 s

Error calling NetCfgDevice::Establish(): ... Timeout was reached
Failed configuring TUN device (TUN_IFACE_CREATE)

507 of the 900 operations had completed when the caller aborted. The identical
route set installed through a single reused socket (ip -batch) takes ~0.4 s, so
the per-bind() cost is the entire difference between working and not working.
Switching to NAT mode does not help, since the baseline cost is not
mirrored-mode related. The per-socket churn is also being reported to
openvpn3-linux separately.

Diagnostic Logs

Per-operation pattern from strace -f -tt -T on openvpn3-service-netcfg,
repeating once per route:

socket(AF_UNIX, SOCK_DGRAM) -> ioctl(SIOCGIFINDEX) -> close
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
  -> setsockopt(SO_SNDBUF) -> setsockopt(SO_RCVBUF)
  -> bind()          <-- ~9-10 ms, dominant cost
  -> getsockname -> sendmsg -> recvmsg -> close

Within the 5 s Establish() window: 507 bind() calls, 4.601 s in bind()
alone out of 4.939 s total worker syscall time — 93% of the budget.

Possibly related, though a different mechanism: #41443 (mirrored networking drops
replies from a TUN-based VPN adapter). Same kernel and distro; that one is about
packet delivery, this one about syscall latency.

Activity

  1. github-actions commented on Aug 28, 2026

    @github-actions
    Contributor

    Logs are required for review from WSL team

    If this a feature request, please reply with '/feature'. If this is a question, reply with '/question'.
    Otherwise, please attach logs by following the instructions below, your issue will not be reviewed unless they are added. These logs will help us understand what is going on in your machine.

    How to collect WSL logs

    Download and execute collect-wsl-logs.ps1 in an administrative powershell prompt:

    Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/microsoft/WSL/master/diagnostics/collect-wsl-logs.ps1" -OutFile collect-wsl-logs.ps1
    Set-ExecutionPolicy Bypass -Scope Process -Force
    .\collect-wsl-logs.ps1
    

    The script will output the path of the log file once done.

    If this is a networking issue, please use .\collect-wsl-logs.ps1 -LogProfile networking instead, following the instructions in Collect WSL logs for networking issues

    Once completed please upload the output files to this GitHub issue.

    See Collect WSL logs (recommended method).

    If you choose to email these logs instead of attaching them to the bug, please send them to [email protected] with the GitHub issue number in the subject, and include a link to your GitHub issue comment in the message body, and reply with '/emailed-logs'.

  2. icicimov commented on Aug 28, 2026

    @icicimov
    Author

    Two problems with the log bundle, and some additional data that I think is more
    useful than it would be.

    I can't run the collector. collect-wsl-logs.ps1 is
    #Requires -RunAsAdministrator and this is a managed device where I don't have
    local admin. So neither the default nor the -LogProfile networking variant is
    available to me.

    Its contents can't go in a public issue. The script runs
    Get-NetIPConfiguration, Get-NetRoute, Get-NetFirewallRule, netsh and
    pktmon/wpr traces. On this machine Get-NetRoute enumerates a corporate VPN
    split-tunnel of ~900 prefixes, along with hostname, DNS suffixes and internal
    addressing. I'm not able to publish that. If the logs are genuinely required I
    can ask for a one-off elevated run and send them to [email protected]
    referencing this issue rather than attaching them here — but see below on whether
    they'd tell you anything.

    This issue carries no machine-specific state. The whole report is a ~20 line
    Python script with a control, and the effect is a fixed per-call cost, not a
    race, a hang or a crash. Anyone can reproduce it on a clean WSL2 install in
    under a minute. There is no trace window to capture — every single bind() is
    slow, all 400 of them.

    Correction to my own speculation

    I suggested the ~10 ms figure looked like a scheduler tick. That is wrong —
    this kernel is CONFIG_HZ=250, i.e. a 4 ms tick, and 10 ms is not a multiple of
    4 ms. Relevant config:

    CONFIG_HZ_250=y
    CONFIG_HZ=250
    CONFIG_NO_HZ_IDLE=y
    CONFIG_HIGH_RES_TIMERS=y
    CONFIG_PREEMPT_DYNAMIC=y
    

    What the distribution actually looks like

    n=400 per family, 2 ms buckets, host namespace:

    AF_NETLINK bind()
       2-  4 ms    5
       4-  6 ms    3
       6-  8 ms    1
       8- 10 ms   91
      10- 12 ms  299      <- 97% of samples fall in 8-12 ms
      16- 18 ms    1
    
    AF_INET/UDP bind()
      10- 12 ms    7
      12- 14 ms    6
      14- 16 ms    2
      16- 18 ms    4
      18- 20 ms    1
      20- 22 ms  361      <- 90% of samples fall in 20-22 ms
      22- 24 ms   15
      24- 26 ms    2
      28- 30 ms    1
      30- 32 ms    1
    

    Sharply unimodal, and AF_INET sits at almost exactly 2x AF_NETLINK. That
    looks like one and two instances of a fixed ~10 ms synchronous cost rather than
    anything timer-aligned. socket() creation in the same loop remains ~0.000 ms.

    This is not a Python measurement artifact — strace -f -tt -T on
    openvpn3-service-netcfg (C++) independently shows the same thing on the raw
    syscall:

    bind(8, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0 <0.010784>
    bind(8, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0 <0.009266>
    bind(8, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0 <0.009291>
    bind(8, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0 <0.009529>
    bind(8, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0 <0.009174>
    

    901 such calls across that trace: mean 9.1 ms, max 10.8 ms, total 8.183 s.

    The question, restated

    Is a fixed ~10 ms bind() expected under WSL2? It applies to AF_UNIX and
    AF_NETLINK, which need no host coordination, and it persists inside a private
    network namespace — so it is not the mirrored-mode port coordination that
    plausibly explains the extra ~10 ms on AF_INET.

    Happy to run any specific unprivileged command, or a narrower elevated capture
    if you can tell me which one component you need — I'd rather do that than ship a
    full ETW bundle of a corporate network configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions