Description
Go version
go version go1.24.3 windows/amd64
Output of go env
in your module/workspace:
set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=1
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\user\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\user\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\user\AppData\Local\Temp\go-build2232190750=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=D:\Code\Archive\Go\serve\go.mod
set GOMODCACHE=C:\Users\user\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\user\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\user\AppData\Roaming\go\telemetry
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.24.3
set GOWORK=
set PKG_CONFIG=pkg-config
What did you do?
package main
import (
"net/http"
)
func main() {
http.ListenAndServe("127.0.0.1:8659", http.FileServer(http.Dir(`F:\LargeFiles`)))
}
Start a simple HTTP static file server on Windows using the above code.
Then, initiate more than 2 concurrent requests using curl
or any other HTTP client.
What did you see happen?
It can be seen that only 2 of the 4 requests, #1 and #2, are receiving data. #3 and #4 are stuck after receiving 512 bytes (presumably the HTTP headers). They remain pending until one of the active transfers complete.
These transfers can be initiated from the same device or multiple different devices. This is a server-side limit. The server cannot concurrently transmit file data to more than 2 requests at a time.
I have identified the root cause. Here's a rough trace of it:
*http.fileHandler.ServeHTTP
(net/http/fs.go)http.serveFile
http.serveContent
io.CopyN
(io/io.go)io.Copy
io.copyBuffer
- In
io.copyBuffer
,src
is a*io.LimitedReader
for the file, anddst
is a*http.response
- Since
dst
has aReadFrom
function, this exit path is taken:if rf, ok := dst.(ReaderFrom); ok { return rf.ReadFrom(src) }
*http.response.ReadFrom
(net/http/server.go).ReadFrom(src)
onw.conn.rwc
, the underlying connection, a*net.TCPConn
*net.TCPConn.ReadFrom
(net/tcpsock.go)*net.TCPConn.readFrom
(net/tcpsock_posix.go)net.sendFile
(net/sendfile_windows.go)poll.SendFile
(internal/poll/sendfile_windows.go)syscall.TransmitFile
(syscall/zsyscall_windows.go)- Windows API call,
TransmitFile
inmswsock.dll
Ultimately, TransmitFile
from Windows API is called.
Here's the documentation for that function: https://learn.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-transmitfile
Under remarks, the following is stated:
Workstation and client versions of Windows optimize the TransmitFile function for minimum memory and resource utilization by limiting the number of concurrent TransmitFile operations allowed on the system to a maximum of two. On Windows Vista, Windows XP, Windows 2000 Professional, and Windows NT Workstation 3.51 and later only two outstanding TransmitFile requests are handled simultaneously; the third request will wait until one of the previous requests is completed.
This limitation is the cause of this issue.
Note that this is not a problem on server versions of Windows:
Server versions of Windows optimize the TransmitFile function for high performance. On server versions, there are no default limits placed on the number of concurrent TransmitFile operations allowed on the system.
What did you expect to see?
All requests should concurrently receive data.
If I edit C:\Program Files\Go\src\net\sendfile_windows.go
to make the net.sendFile
function immediately return 0, nil, false
, the TransmitFile
call is avoided. The caller (*net.TCPConn.readFrom
) falls back to other methods of transferring the file and all HTTP requests receive data concurrently as expected.
func sendFile(fd *netFD, r io.Reader) (written int64, err error, handled bool) {
return 0, nil, false
}
I think the solution for this would be to change the standard library's behaviour to avoid using the TransmitFile
Windows API call while running on workstation and client versions of Windows.