powershelldba.de · Uwe Janke

Cannot Load Counter Name Data: When SQL Server Setup Fails on Broken Performance Counters

A clean Windows box, fresh SQL Server 2022 media, and setup dies partway through with a message about an invalid index in the registry. Nothing in it mentions SQL Server, because the fault is not SQL Server's. Here is what the installer was actually doing, why the damage is usually older than the installation, and a read-only pre-flight check that finds it before you start.

The error reads:

Cannot load Counter Name data because an invalid index was read from the registry.

It shows up during configuration, not during the file copy, which is what makes it expensive. By the time it appears, setup has been running for a while and now has to roll back. The message says nothing about SQL Server, and that is accurate: it is a Windows performance counter problem that SQL Server setup happens to be the first program to notice.

What Setup Was Actually Doing

SQL Server setup does more than copy files and create services. During the configuration phase it registers its own performance counter objects, the ones you later query as SQLServer:Buffer Manager, SQLServer:SQL Statistics, SQLServer:Databases and the rest. Registration happens through the Windows tool lodctr, pointed at the instance's own definition file:

C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Binn\sqlctr.ini

Appending counters means reading the existing list first, because the new entries have to be assigned index numbers that continue where the current ones end. That read is the step that fails. If the existing list is already inconsistent, setup cannot work out where its own counters go, the configuration step returns an error, and the installation unwinds.

The sequence is worth stating plainly, because it decides where you look: setup did not corrupt anything. Setup read something that was already corrupt.

Where the Counter Names Actually Live

Windows keeps performance counter definitions in several places at once, and this error is what happens when they stop agreeing with each other.

Component What it holds
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib Last Counter and Last Help: the highest index number currently handed out. This is the ceiling.
...\Perflib\009 (English), \0007 (German) The Counter and Help multi-string values: a long alternating list of index number, then name, then index number, then name.
C:\Windows\System32\perfc009.dat
C:\Windows\System32\perfh009.dat
The base counter and help strings that ship with Windows. Always present on a healthy system.
C:\Windows\System32\PerfStringBackup.INI The backup store. This is what lodctr /R restores from, and the detail that decides whether the repair works.
HKLM\SYSTEM\CurrentControlSet\Services\<svc>\Performance One key per provider: its DLL, its Open/Collect/Close entry points, its index range, and often a PerfIniFile value naming its definition file.

"Invalid index" is what you get when the list contains an index number that the ceiling does not cover, or something that is not a number where a number belongs. A reader walks the list, hits an index beyond Last Counter, and refuses to continue. That is the entire failure, and it is why the message is about an index rather than about a name.

Why It Was Already Broken

Every product that ships performance counters appends to that shared list, and the append is not transactional in any meaningful sense. The usual causes, in rough order of how often they turn up:

Not every provider owns an .ini file, which is worth knowing before you go hunting for missing ones. On a current Windows build there are around three dozen services with a Performance key, in three shapes:

A rebuild does not touch V2 providers. Modern Windows components, and .NET Core / .NET 5 and later, register counters from XML manifests instead of .ini files. They live under Perflib\_V2Providers and are registered with lodctr /M:. A lodctr /R neither repairs nor damages them. If you are chasing a counter problem in a modern component, the classic rebuild is simply the wrong tool.

Check Before You Install, Not After It Rolls Back

The cheapest and most decisive test is to make Windows perform the same read that setup performs. If this throws, setup will fail:

Get-Counter -ListSet *

Or without PowerShell, which is useful when you are looking at a server through someone else's remote session:

typeperf -qx

A healthy machine enumerates a couple of hundred counter sets. A broken one returns the same invalid index message you would otherwise have discovered halfway through the installation.

That covers the symptom. To find the cause, and to catch the neighbouring problems that also stop a clean install, the pre-flight script below runs five checks:

  1. Counter name enumeration. The exact read setup performs.
  2. Perflib index range. Last Counter must be greater than or equal to the highest numeric index in the string list. A higher index in the list is the literal definition of the error.
  3. Orphaned providers. Every Performance key whose PerfIniFile no longer exists under C:\Windows\INF\, reported by service name so you can see which uninstall left the mess behind.
  4. WMI performance classes. The WMI adapter reads the same data. A broken adapter bites you later during monitoring even when setup passes.
  5. Pending reboot. Unrelated to counters, but the other common reason a perfectly good installation fails.

It is read-only. It changes no registry value, no service and no file. If it finds a problem it prints the repair steps rather than running them.

📦 Download the pre-flight check

A small ZIP with Test-PerfCounterHealth.ps1 and a README. Plain, readable PowerShell, nothing compiled or obfuscated, so you can open it in a text editor and see exactly what it reads before you run it.

⬇ Download sql-perfcounter-preflight.zip

~3 KB · Test-PerfCounterHealth.ps1 + README.txt · read-only, changes nothing · tested on Windows PowerShell 5.1 and PowerShell 7 · exit code 0 = healthy, 1 = fix required

Run it elevated, because reading the service Performance keys needs it:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Test-PerfCounterHealth.ps1

On a healthy server it looks like this:

[1] Counter name enumeration ... OK (208 counter sets)
[2] Perflib index range ... OK (Last Counter 17870, highest index 17870)
[3] Orphaned counter providers ... none
[4] WMI performance classes ... OK
[5] Pending reboot ... no

Performance counters healthy, safe to run SQL Server setup.

The exit code makes it usable as a gate in a build pipeline or an unattended installation wrapper, so a broken box never reaches setup in the first place. Here is the whole script, in case you would rather copy it than download it:

<#
.SYNOPSIS
    Pre-flight check for Windows performance counter health before a SQL Server installation.

.DESCRIPTION
    SQL Server setup registers its own performance counters (SQLServer:Buffer Manager and
    friends) through lodctr against sqlctr.ini. Before it writes, it reads the existing
    counter list. If that list is inconsistent, setup fails with:

        Cannot load Counter Name data because an invalid index was read from the registry.

    The damage is almost always older than the installation: an uncleanly removed monitoring
    or backup agent leaves a Services\<svc>\Performance key pointing at an .ini file that no
    longer exists, or the Perflib index range no longer covers the counter string list.

    This script runs the same reads setup performs, plus the checks that identify the cause,
    so the problem is found before setup rolls back halfway through.

.NOTES
    Run elevated. Read-only, changes nothing.
    Exit code 0 = healthy, 1 = fix required before running setup.
    Tested on Windows PowerShell 5.1 and PowerShell 7.

    https://www.powershelldba.de/blog/articles/sql-server-setup-invalid-counter-name-index.html
#>

[CmdletBinding()]
param()

$fail = $false

# 1) Can Windows enumerate counter names at all? This is the exact read setup trips over.
Write-Host "[1] Counter name enumeration ..." -NoNewline
try {
    $sets = Get-Counter -ListSet * -ErrorAction Stop
    Write-Host " OK ($($sets.Count) counter sets)" -ForegroundColor Green
} catch {
    Write-Host " FAILED: $($_.Exception.Message)" -ForegroundColor Red
    $fail = $true
}

# 2) Index range sane? Last Counter must cover the highest index in the string list.
Write-Host "[2] Perflib index range ..." -NoNewline
$pl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib'
$lastCounter = (Get-ItemProperty $pl -ErrorAction SilentlyContinue).'Last Counter'
$lang = Get-ChildItem $pl -ErrorAction SilentlyContinue |
        Where-Object { $_.PSChildName -match '^\d{3,4}$' } | Select-Object -First 1
if ($lang) {
    $idx = (Get-ItemProperty $lang.PSPath).Counter |
           Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ }
    $max = ($idx | Measure-Object -Maximum).Maximum
    if ($max -gt $lastCounter) {
        Write-Host " MISMATCH: highest index $max > Last Counter $lastCounter" -ForegroundColor Red
        $fail = $true
    } else {
        Write-Host " OK (Last Counter $lastCounter, highest index $max)" -ForegroundColor Green
    }
} else {
    Write-Host " strings held in perfc/perfh .dat, covered by check 1 (Last Counter $lastCounter)" -ForegroundColor Yellow
}

# 3) Orphaned providers: a Performance key pointing at an .ini that is gone.
Write-Host "[3] Orphaned counter providers ..." -NoNewline
$orphans = @()
foreach ($s in Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Services -ErrorAction SilentlyContinue |
                Where-Object { Test-Path "$($_.PSPath)\Performance" }) {
    $ini = (Get-ItemProperty "$($s.PSPath)\Performance" -ErrorAction SilentlyContinue).PerfIniFile
    if ($ini -and -not (Get-ChildItem 'C:\Windows\INF' -Filter $ini -Recurse -File -ErrorAction SilentlyContinue |
                        Select-Object -First 1)) {
        $orphans += "$($s.PSChildName) -> $ini"
    }
}
if ($orphans) {
    Write-Host " $($orphans.Count) found" -ForegroundColor Red
    $orphans | ForEach-Object { Write-Host "      $_" -ForegroundColor Red }
    $fail = $true
} else {
    Write-Host " none" -ForegroundColor Green
}

# 4) WMI perf adapter, the other half of the same plumbing.
Write-Host "[4] WMI performance classes ..." -NoNewline
try {
    $null = Get-CimInstance Win32_PerfFormattedData_PerfOS_System -ErrorAction Stop
    Write-Host " OK" -ForegroundColor Green
} catch {
    Write-Host " FAILED: $($_.Exception.Message)" -ForegroundColor Red
    $fail = $true
}

# 5) Pending reboot: unrelated to counters, but the other reason a clean install fails.
Write-Host "[5] Pending reboot ..." -NoNewline
$pending = (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') -or
           (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') -or
           ($null -ne (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -ErrorAction SilentlyContinue).PendingFileRenameOperations)
if ($pending) { Write-Host " YES, reboot first" -ForegroundColor Red; $fail = $true }
else          { Write-Host " no" -ForegroundColor Green }

Write-Host ""
if ($fail) {
    Write-Host "Fix before running SQL Server setup:" -ForegroundColor Yellow
    Write-Host '  1. reg export "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib" perflib_backup.reg'
    Write-Host '  2. ren C:\Windows\System32\PerfStringBackup.INI PerfStringBackup.INI.old'
    Write-Host '  3. lodctr /R          (run twice if the first pass errors)'
    Write-Host '  4. winmgmt /resyncperf'
    Write-Host '  5. reboot, then re-run this check'
    exit 1
} else {
    Write-Host "Performance counters healthy, safe to run SQL Server setup." -ForegroundColor Green
    exit 0
}

Repairing a Broken Counter Registry

The repair rebuilds the counter strings from the backing files. The order matters more than most write-ups admit:

reg export "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib" C:\Admin\perflib_backup.reg

ren C:\Windows\System32\PerfStringBackup.INI PerfStringBackup.INI.old

lodctr /R
lodctr /R

winmgmt /resyncperf

Then reboot and re-run the check.

Two details in there do the actual work.

Renaming the backup first

lodctr /R restores from PerfStringBackup.INI. If that snapshot was written after the corruption crept in, the rebuild faithfully restores the corruption, and you conclude, wrongly, that the fix does not work. Renaming it forces the rebuild to reconstruct from the base .dat files plus a fresh walk of every service Performance key. Providers whose .ini is genuinely gone get dropped at that point, which is precisely the outcome you want: the orphan was the problem.

The two meanings of /R

lodctr /R with no argument rebuilds from the backup store. lodctr /R:<file> restores from that named file. They look like the same switch and they are not. Mixing them up is a common reason someone reports that they ran the command and nothing changed. Running the bare form twice is normal practice: the first pass sometimes errors while it is still working from an inconsistent state, and the second completes cleanly.

Export the key first, and do this in a maintenance window. Rebuilding the counter registry is not risky in the sense of losing data, but it does briefly disturb everything that reads counters: monitoring agents, collectors, and any dashboard watching that host will see a gap and may alert on it. On a 64-bit system you may also have to run lodctr /R from C:\Windows\SysWOW64 as well as from C:\Windows\System32 when 32-bit providers are involved.

When the Problem Is SQL Server's Own File

If the counter registry checks out clean and setup still fails at the same step, the suspect moves to the instance's own definition file. Setup calls unlodctr to remove any previous registration, then lodctr against sqlctr.ini in the instance's Binn folder. You can re-run that by hand:

cd "C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Binn"
unlodctr MSSQLSERVER
lodctr sqlctr.ini

For a named instance, substitute MSSQL$INSTANCENAME. If sqlctr.ini itself is missing, no amount of registry rebuilding will help and you are into repairing the instance.

Either way, read the setup logs rather than the dialog. The summary points at the failing step, and the detail log carries the actual return code from the counter registration:

C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log\Summary.txt
C:\Program Files\Microsoft SQL Server\160\Setup Bootstrap\Log\<timestamp>\Detail.txt

Search Detail.txt for lodctr or perfctr. The dialog shows you the symptom; the log shows you which call returned it.

Preventing It on the Next Build

You cannot prevent this during setup, because there is no setup option involved. What you can prevent is discovering it during setup.

After the installation, confirm that SQL Server's own counters actually arrived. A setup that completes is not by itself proof that registration succeeded:

Get-Counter -ListSet "SQLServer:Buffer Manager" |
    Select-Object -ExpandProperty Paths

The Point Worth Keeping

SQL Server sits on a large amount of Windows plumbing that nobody thinks about until it breaks: performance counters, WMI, MSDTC, the .NET and VC++ runtimes, the pending-reboot state. Setup exercises all of it, which makes the installer an unusually good detector of pre-existing Windows damage, and an unusually bad place to discover it.

When an installation fails on a message that never mentions the product, the useful instinct is not to re-download the media. It is to ask which Windows component the installer was talking to at that moment, and then to check that component before the next install rather than after the next rollback.