Skip to content

net/http: FileServer is limited to only 2 concurrent file transfers due to TransmitFile limitation on Windows #73746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
shibijm opened this issue May 16, 2025 · 7 comments
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsFix The path to resolution is known, but the work has not been done.

Comments

@shibijm
Copy link
Contributor

shibijm commented May 16, 2025

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.

Image

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, and dst is a *http.response
  • Since dst has a ReadFrom 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) on w.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 in mswsock.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
}

Image

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.

@odeke-em
Copy link
Member

Great investigation and thank you very much for this filing @shibijm. Kindly pinging @alexbrainman @qmuntal.

@odeke-em odeke-em added the NeedsFix The path to resolution is known, but the work has not been done. label May 16, 2025
@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label May 16, 2025
@alexbrainman
Copy link
Member

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.

It is possible to determine that we are running on client version of Windows? If yes, maybe we could avoid using TransmitFile there. Leaving for @qmuntal to decide.

Alex

@shibijm
Copy link
Contributor Author

shibijm commented May 17, 2025

It is possible to determine that we are running on client version of Windows?

One option is the RtlGetVersion function:
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlgetversion

It's being used in internal/syscall/windows/version_windows.go for a different purpose for instance:

// According to documentation, RtlGetVersion function always succeeds.
//sys rtlGetVersion(info *_OSVERSIONINFOW) = ntdll.RtlGetVersion
// Version retrieves the major, minor, and build version numbers
// of the current Windows OS from the RtlGetVersion API.
func Version() (major, minor, build uint32) {
info := _OSVERSIONINFOW{}
info.osVersionInfoSize = uint32(unsafe.Sizeof(info))
rtlGetVersion(&info)
return info.majorVersion, info.minorVersion, info.buildNumber
}

There is also a wrapper for it in golang.org/x/sys/windows. The value of x/sys/windows.RtlGetVersion().ProductType on my PC running Windows 11 Pro is 1 (VER_NT_WORKSTATION), which indicates that it's a workstation/client version of Windows.

Image
(source: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw)

Image
(source: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa)

@alexbrainman
Copy link
Member

One option is the RtlGetVersion function

Looks reasonable. Thank you.

Alex

@odeke-em
Copy link
Member

@shibijm could you perhaps send a change list for this which will spark off the code review, if possible? Thank you.

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/673855 mentions this issue: net: bypass concurrency-limited WinAPI TransmitFile function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsFix The path to resolution is known, but the work has not been done.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants