Skip to content

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

Closed
@shibijm

Description

@shibijm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions