~/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: 
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 NetAdapter and NetTCPIP modules. 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* and Set-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 -NoRestart and 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

NameTypeRequiredDescription
AdapterNamestringYesOne or more adapter names, as shown by Get-NetAdapter.
RssMaxProcessorsintNoMaximum processors RSS may use per adapter (Set-NetAdapterRss -MaxProcessors). Only changed when supplied.
RssBaseProcessorNumberintNoFirst processor RSS may use (-BaseProcessorNumber), for example 2 to keep RSS off cores 0 and 1. Only changed when supplied.
EnableJumboFrameswitchNoSets the standardized *JumboPacket keyword to JumboPacketSize, if the driver exposes it.
JumboPacketSizeintNoFrame size in bytes including the 14-byte Ethernet header. Default 9014 (a 9000-byte MTU).
NoRestartswitchNoPasses -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):

powershell
.\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):

powershell
.\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:

powershell
.\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):

text
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":

powershell
ping.exe -f -l 8972 <storage-target-ip>

Script

powershell
<#
.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, CTCP and DCTCP. DCTCP relies 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 Disabled accepts the value, but look at why it was enabled first.
  • Legacy registry tweaks. Microsoft lists TcpWindowSize, NumTcbTablePartitions and MaxHashTableSize under Tcpip\Parameters as "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." Use Set-NetTCPSetting -AutoTuningLevelLocal (or netsh interface tcp set global autotuninglevel=) instead. Normal is the default. Disabled pins 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 set BaseProcessorNumber and MaxProcessors when 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 IPv4OperationalState is False, read IPv4FailureReason. The documented reasons are NicPropertyDisabled (run Enable-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) and NetOffloadGlobalDisabled (run Set-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: check Get-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-SmbMultichannelConnection during a copy. It's on by default.
  • Validate before and after with a real throughput test (ntttcp or iperf3) between the two endpoints, not the settings alone. A firmware or driver bug can undo any of this.

Source