Here are my answers to your questions after reviewing on them along with the available Microsoft documents:
1. Is this a known Windows TCP behavior?
The public documentation doesn't describe this ~10 ms stall as expected behavior or as a known issue on your build. TCP_NODELAY disables Nagle's algorithm, but the documentation doesn't promise a maximum transmission delay (IPPROTO_TCP socket options).
The two-connection result suggests the delay is linked to send and receive activity happening on the same connection. It doesn't prove a specific cause, such as lock contention, a timer or scheduling. Given your ACK, window and retransmission observations, delayed ACKs alone also don't explain it.
2. Can it be avoided while keeping one bidirectional connection?
The current evidence doesn't confirm a single-connection fix. If both sender threads call send concurrently, it's worth testing with one sending thread per process. Keep the receive path and workload the same.
The WSASend documentation advises against calling WSASend from multiple threads at the same time on one stream socket, because some providers can interleave the data. That warning is about data ordering, so it doesn't show that concurrent sends cause the stalls. If your sends are already serialized, you can skip this test.
Splitting the traffic across two connections reduced the delays in your runs, but that hasn't been proven as a fix. Avoid SIO_LOOPBACK_FAST_PATH, because it's deprecated (SIO_LOOPBACK_FAST_PATH Control Code).
3. What additional tracing would help?
Capture these together on the same timeline:
- TCP events: TcpSendPosted, TcpSendTransmitted, and the receive/ACK events around them, with connection and sequence-range details. Use netsh trace show provider Microsoft-Windows-TCPIP to see which keywords are available (Using Netsh to manage traces).
- Winsock/AFD events: Add Microsoft-Windows-Winsock-AFD to match socket calls with TCP activity (Control of Winsock tracing).
- CPU and scheduling activity across the whole system: Capture CPU sampling, CSwitch, ReadyThread, DPC/ISR, and process/thread events. Collect stacks for CPU sampling, context switches and ready-thread events, plus TCP event stacks if available. Set these up in a custom WPR profile, then confirm the stacks appear in the ETL (Authoring Recording Profiles).
For one representative stall, look across all CPUs to find what blocks the send and what runs just before the data is transmitted.
To start, please share your current capture commands or WPR profile, one affected ETL and its matching message log. If those traces already include the events and stacks above, you may not need a new capture. Both runs' state.json files would also help confirm the one-connection vs. two-connection comparison.
I hope this helps answering your questions. If you find my response helpful, I would greatly appreciate it if you could follow the guide here so others experiencing similar behavior or issue can benefit from it as well.