~/2025/04/10/powershell-windows-server-optimize-the-tcp-stack-for-10gbe-workloads.md
PowerShell: Windows Server – Tune the TCP Stack for 10GbE Workloads
--- author: Tom Lasswell date: read: 4 min in: [ps, scripts] tags: [powershell, windows, tcp, network] ---
$ grep -n '^#' post.md
Dropping a 10GbE NIC into a Windows Server box and expecting line-rate throughput without looking at it is a common mistake. The Windows defaults are mostly right; what goes wrong is drift. Receive Side Scaling gets switched off or confined to a few cores, a driver update resets offloads, someone leaves receive window autotuning disabled after a troubleshooting session, or an old "TCP optimizer" registry tweak is still sitting in Tcpip\Parameters doing nothing. I run a version of this script against every new file server, backup target and hypervisor host with a 10GbE (or faster) uplink before I trust any throughput numbers. It checks and sets the things Microsoft's network adapter tuning guidance actually recommends, reports the ones it deliberately leaves alone, and prints a summary of where each adapter ended up. The why behind each setting is in Windows Server: TCP Stack Tuning for a 10GbE Storage Network.
Requirements
- Windows Server 2016 or later, elevated PowerShell (Administrator).
- The built-in
NetAdapterandNetTCPIPmodules. Nothing to install. - An adapter whose driver exposes RSS, RSC, checksum offload and LSO. Some virtual NICs don't expose all of them; the script warns and continues. Check with
Get-NetAdapterAdvancedProperty -Name "<adapter>"if you're unsure. - A maintenance window or an out-of-band console. The
Enable-NetAdapter*andSet-NetAdapter*cmdlets restart the adapter by default so the change takes effect; don't run this over the NIC you're connected through unless you pass-NoRestartand restart the adapter later. - For
-EnableJumboFrame: the switch ports and the far end (storage array, other hosts) configured for the same MTU first. A mismatch doesn't fail cleanly; it shows up as drops and retransmits under load.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
AdapterName | string | Yes | One or more adapter names, as shown by Get-NetAdapter. |
RssMaxProcessors | int | No | Maximum processors RSS may use per adapter (Set-NetAdapterRss -MaxProcessors). Only changed when supplied. |
RssBaseProcessorNumber | int | No | First processor RSS may use (-BaseProcessorNumber), for example 2 to keep RSS off cores 0 and 1. Only changed when supplied. |
EnableJumboFrame | switch | No | Sets the standardized *JumboPacket keyword to JumboPacketSize, if the driver exposes it. |
JumboPacketSize | int | No | Frame size in bytes including the 14-byte Ethernet header. Default 9014 (a 9000-byte MTU). |
NoRestart | switch | No | Passes -NoRestart to the adapter cmdlets. Changes then apply at the next adapter or system restart. |
Usage
Audit and tune one adapter with the defaults (RSS on, RSC/checksum/LSO on, autotuning Normal):
.\Optimize-TcpStackFor10GbE.ps1 -AdapterName "SLOT 2 Port 1"
Two storage ports, RSS kept off cores 0 and 1 and capped at 8 processors each, jumbo frames on (switch and array already at MTU 9000):
.\Optimize-TcpStackFor10GbE.ps1 -AdapterName "SLOT 2 Port 1", "SLOT 2 Port 2" -RssBaseProcessorNumber 2 -RssMaxProcessors 8 -EnableJumboFrame
Stage the changes without bouncing the adapters, then restart them in the window:
.\Optimize-TcpStackFor10GbE.ps1 -AdapterName "SLOT 2 Port 1" -NoRestart
Restart-NetAdapter -Name "SLOT 2 Port 1"
Sample output (values depend on the NIC and driver):
WARNING: Legacy value TcpWindowSize is set under Tcpip\Parameters. Windows ignores it; remove it to avoid confusion.
WARNING: TCP Chimney global state is 'Enabled'. Chimney is deprecated; see Notes before changing it.
Name Enabled Profile BaseProcessorNumber MaxProcessors NumberOfReceiveQueues
---- ------- ------- ------------------- ------------- ---------------------
SLOT 2 Port 1 True NUMAStatic 2 8 8
SLOT 2 Port 2 True NUMAStatic 2 8 8
Name IPv4Enabled IPv4OperationalState IPv4FailureReason
---- ----------- -------------------- -----------------
SLOT 2 Port 1 True True NoFailure
SLOT 2 Port 2 True True NoFailure
Name JumboPacket
---- -----------
SLOT 2 Port 1 9014
SLOT 2 Port 2 9014
SettingName AutoTuningLevelLocal CongestionProvider
----------- -------------------- ------------------
Datacenter Normal CUBIC
Internet Normal CUBIC
Then prove the jumbo path end to end. ping -f sets Don't Fragment and -l is the ICMP payload: 9000 minus 20 bytes of IPv4 header and 8 bytes of ICMP header is 8972. If any hop is still at 1500, this fails with "Packet needs to be fragmented but DF set":
ping.exe -f -l 8972 <storage-target-ip>
Script
<#
.SYNOPSIS
Audits and tunes NIC offloads and TCP autotuning on Windows Server for 10GbE (or faster) adapters.
.DESCRIPTION
Enables Receive Side Scaling (optionally setting the base processor and
processor cap), Receive Segment Coalescing, checksum offload and Large Send
Offload on the named adapters, optionally sets the *JumboPacket keyword, and
makes sure TCP receive window autotuning is at its default, Normal. Reports,
without changing, the TCP Chimney global state and any legacy Tcpip registry
values that Windows ignores. Prints a summary of the resulting settings.
.PARAMETER AdapterName
One or more network adapter names, as shown by Get-NetAdapter.
.PARAMETER RssMaxProcessors
Maximum number of processors RSS may use per adapter. Only changed when supplied.
.PARAMETER RssBaseProcessorNumber
First processor number RSS may use. Only changed when supplied.
.PARAMETER EnableJumboFrame
Sets the *JumboPacket keyword to JumboPacketSize, if the adapter exposes it.
.PARAMETER JumboPacketSize
Frame size in bytes including the Ethernet header. Default 9014.
.PARAMETER NoRestart
Passes -NoRestart to the adapter cmdlets; changes apply at the next adapter restart.
.EXAMPLE
.\Optimize-TcpStackFor10GbE.ps1 -AdapterName "SLOT 2 Port 1", "SLOT 2 Port 2" -RssBaseProcessorNumber 2 -RssMaxProcessors 8 -EnableJumboFrame
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-04-10)
Requires: Windows Server 2016 or later, NetAdapter and NetTCPIP modules, Administrator
.LINK
https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-performance-tuning-nics
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string[]]$AdapterName,
[ValidateRange(1, 256)]
[int]$RssMaxProcessors,
[ValidateRange(0, 255)]
[int]$RssBaseProcessorNumber,
[switch]$EnableJumboFrame,
[ValidateRange(1514, 65535)]
[int]$JumboPacketSize = 9014,
[switch]$NoRestart
)
$restartParam = @{ NoRestart = [bool]$NoRestart }
# Confirm every adapter exists before changing anything.
foreach ($name in $AdapterName) {
Get-NetAdapter -Name $name -ErrorAction Stop | Out-Null
}
# Report legacy Windows Server 2003-era values that Microsoft documents as ignored.
$tcpipParams = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"
foreach ($legacy in @("TcpWindowSize", "NumTcbTablePartitions", "MaxHashTableSize")) {
if ($null -ne $tcpipParams.$legacy) {
Write-Warning "Legacy value $legacy is set under Tcpip\Parameters. Windows ignores it; remove it to avoid confusion."
}
}
# Report TCP Chimney. Microsoft: deprecated in Windows Server 2016, don't use it.
$offloadGlobal = Get-NetOffloadGlobalSetting
if ($offloadGlobal.Chimney -ne "Disabled") {
Write-Warning "TCP Chimney global state is '$($offloadGlobal.Chimney)'. Chimney is deprecated; see Notes before changing it."
}
if ($offloadGlobal.ReceiveSideScaling -ne "Enabled") {
Write-Warning "Global Receive Side Scaling is '$($offloadGlobal.ReceiveSideScaling)'. Enable it with Set-NetOffloadGlobalSetting -ReceiveSideScaling Enabled."
}
foreach ($name in $AdapterName) {
# Receive Side Scaling: on, with an optional processor range.
try {
Enable-NetAdapterRss -Name $name @restartParam -ErrorAction Stop
$rssParams = @{}
if ($PSBoundParameters.ContainsKey("RssMaxProcessors")) {
$rssParams.MaxProcessors = $RssMaxProcessors
}
if ($PSBoundParameters.ContainsKey("RssBaseProcessorNumber")) {
$rssParams.BaseProcessorNumber = $RssBaseProcessorNumber
}
if ($rssParams.Count -gt 0) {
Set-NetAdapterRss -Name $name @rssParams @restartParam -ErrorAction Stop
}
} catch {
Write-Warning "RSS on '$name': $($_.Exception.Message)"
}
# Receive Segment Coalescing, checksum offload and LSO: on where the driver supports them.
try {
Enable-NetAdapterRsc -Name $name @restartParam -ErrorAction Stop
} catch {
Write-Warning "RSC on '$name': $($_.Exception.Message)"
}
try {
Enable-NetAdapterChecksumOffload -Name $name @restartParam -ErrorAction Stop
} catch {
Write-Warning "Checksum offload on '$name': $($_.Exception.Message)"
}
try {
Enable-NetAdapterLso -Name $name @restartParam -ErrorAction Stop
} catch {
Write-Warning "LSO on '$name': $($_.Exception.Message)"
}
# Jumbo frames through the standardized keyword, not the vendor display name.
if ($EnableJumboFrame) {
$jumbo = Get-NetAdapterAdvancedProperty -Name $name -RegistryKeyword "*JumboPacket" -ErrorAction SilentlyContinue
if (-not $jumbo) {
Write-Warning "Adapter '$name' does not expose *JumboPacket. Skipping."
} elseif ($jumbo.ValidRegistryValues -and ($jumbo.ValidRegistryValues -notcontains [string]$JumboPacketSize)) {
Write-Warning "Adapter '$name' accepts *JumboPacket values $($jumbo.ValidRegistryValues -join ', '), not $JumboPacketSize. Skipping."
} else {
Set-NetAdapterAdvancedProperty -Name $name -RegistryKeyword "*JumboPacket" -RegistryValue $JumboPacketSize @restartParam -ErrorAction Stop
}
}
}
# Receive window autotuning: make sure it is at the default, Normal.
$tuning = Get-NetTCPSetting | Where-Object { $_.AutoTuningLevelLocal -and $_.AutoTuningLevelLocal -ne "Normal" }
if ($tuning) {
Write-Warning "AutoTuningLevelLocal is not Normal on: $(($tuning | ForEach-Object { $_.SettingName }) -join ', '). Setting Normal."
Set-NetTCPSetting -AutoTuningLevelLocal Normal
}
# Summary.
Get-NetAdapterRss -Name $AdapterName |
Format-Table -Property Name, Enabled, Profile, BaseProcessorNumber, MaxProcessors, NumberOfReceiveQueues -AutoSize
Get-NetAdapterRsc -Name $AdapterName |
Format-Table -Property Name, IPv4Enabled, IPv4OperationalState, IPv4FailureReason -AutoSize
if ($EnableJumboFrame) {
Get-NetAdapterAdvancedProperty -Name $AdapterName -RegistryKeyword "*JumboPacket" -ErrorAction SilentlyContinue |
Format-Table -Property Name, @{ Name = "JumboPacket"; Expression = { $_.RegistryValue -join "," } } -AutoSize
}
Get-NetTCPSetting -SettingName Datacenter, Internet |
Format-Table -Property SettingName, AutoTuningLevelLocal, CongestionProvider -AutoSize
Notes
- What the script deliberately doesn't touch. The congestion provider: Windows Server 2019 moved the Internet, Datacenter and both Custom templates to CUBIC (Compat stays NewReno), and the cmdlet only accepts
Default,CTCPandDCTCP.DCTCPrelies on ECN marking from the switches, so it's a fabric decision, not a per-host tweak. TCP Chimney: Microsoft says don't use Chimney or IPsec Task Offload ("deprecated in Windows Server 2016, and might adversely affect server and networking performance"). If the warning fires,Set-NetOffloadGlobalSetting -Chimney Disabledaccepts the value, but look at why it was enabled first. - Legacy registry tweaks. Microsoft lists
TcpWindowSize,NumTcbTablePartitionsandMaxHashTableSizeunderTcpip\Parametersas "no longer supported, and are ignored," and "starting with Windows Server 2019, you can no longer use the registry to configure the TCP receive window size." UseSet-NetTCPSetting -AutoTuningLevelLocal(ornetsh interface tcp set global autotuninglevel=) instead.Normalis the default.Disabledpins the window at its default size (64 KB on links from 100 Mbps to 10 Gbps, 128 KB at 10 Gbps and up), which caps a single connection's throughput. - RSS profile. The default profile is
NUMAStatic. Microsoft's cmdlet help says "selecting the correct profile should be sufficient in most scenarios," so only setBaseProcessorNumberandMaxProcessorswhen you're partitioning cores between several adapters. Don't mix RSS-capable and non-RSS adapters for the same traffic; the tuning guide warns this can "severely" degrade performance. - RSC can be enabled and still not operate. If
IPv4OperationalStateisFalse, readIPv4FailureReason. The documented reasons areNicPropertyDisabled(runEnable-NetAdapterRsc),WFPCompatibility(a Windows Filtering Platform filter, typically security software, is in the way),NDISCompatibility(driver older than NDIS 6.30),ForwardingEnabled(IP forwarding is on) andNetOffloadGlobalDisabled(runSet-NetOffloadGlobalSetting -ReceiveSegmentCoalescing Enabled). - Receive buffers. For receive-heavy workloads Microsoft recommends raising the driver's receive buffers (
*ReceiveBuffers) to the maximum, because low values cause drops. The range is vendor-defined: checkGet-NetAdapterAdvancedProperty -Name "<adapter>" -RegistryKeyword "*ReceiveBuffers"before setting it. - SMB storage traffic. With one RSS-capable NIC, SMB Multichannel opens several TCP connections per session so a single file copy isn't stuck on one core. Check it with
Get-SmbMultichannelConnectionduring a copy. It's on by default. - Validate before and after with a real throughput test (
ntttcporiperf3) between the two endpoints, not the settings alone. A firmware or driver bug can undo any of this.
Source
- Network adapter performance tuning in Windows Server
- Set-NetAdapterRss
- Set-NetAdapterAdvancedProperty
- Standardized INF keywords that can be edited (*JumboPacket, *ReceiveBuffers)
- Set-NetTCPSetting
- Set-NetOffloadGlobalSetting
- MSFT_NetAdapterRscSettingData class (RSC failure reasons)
- Windows Transport converges on two congestion providers: Cubic and LEDBAT
- ping command reference