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.datC:\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:
- An uncleanly removed agent. Monitoring, backup and APM agents register counters at install time. If the uninstall is interrupted, or the product is ripped out by deleting folders, the
Performanceregistry key survives while its definition file does not. - A previous SQL Server that was removed badly. Same mechanism, and the reason this error clusters on rebuilt boxes rather than genuinely new ones.
- An image or template. If the golden image was captured with the inconsistency already in it, every server built from it inherits the problem, and it will look like a coincidence that they all fail the same way.
- An interrupted installation of anything at all. A reboot in the middle of counter registration leaves the ceiling raised but the list incomplete, or the reverse.
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:
- With a
PerfIniFile: BITS, MSDTC, ESENT, TapiSrv, WmiApRpl, the .NET Framework providers and so on. Windows keeps a copy of each definition file underC:\Windows\INF\<ProviderName>\<langid>\, one folder per language (0000for neutral,0407for German). That copy exists specifically so a rebuild can replay the registration. - Without any
.ini: PerfOS, PerfProc, PerfDisk, PerfNet, Tcpip, NetBT, Spooler, Dnscache. These use the base strings from the.datfiles and carry only an index range. Nothing to lose, nothing to repair. - Pointing at a file that is gone. This is the broken shape, and the one worth hunting: a
Performancekey whosePerfIniFilehas no matching file anywhere underC:\Windows\INF\.
.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:
- Counter name enumeration. The exact read setup performs.
- Perflib index range.
Last Countermust 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. - Orphaned providers. Every
Performancekey whosePerfIniFileno longer exists underC:\Windows\INF\, reported by service name so you can see which uninstall left the mess behind. - WMI performance classes. The WMI adapter reads the same data. A broken adapter bites you later during monitoring even when setup passes.
- 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.
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.
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.
- Run the check as part of server handover. Before any database software touches a new box, and certainly before that box becomes a golden image. Thirty seconds of read-only checks against a rolled-back installation is not a close call.
- Take a known-good snapshot once the server is validated.
lodctr /S:C:\Admin\perf_clean.iniwrites a counter-string snapshot you control, so a later repair has something trustworthy to restore from instead of aPerfStringBackup.INIof unknown vintage. - Treat agent removal as a change. The single most common cause is a monitoring or backup agent that was uninstalled by deleting its folder. When an agent leaves, verify the counter list then, not six months later when a database installation fails.
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.