nu1301 unable to load service index, and which source failed
A nu1301 unable to load service index error means dotnet restore asked a package source for its V3 index and did not get a usable answer, so it never got as far as looking for your packages. The message always names the source, and the second line, which people skip, carries the reason. Read those two lines together and the error resolves into one of three ordinary situations.

What this error means
Restore fails early, before any package is mentioned, with a line that names your project file, the code NU1301, and a source URL. A second NU1301 line follows, indented, carrying the underlying failure. NuGet documents NU1301 as "Restore could not be completed because the listed source is unavailable", which covers a remote feed that will not answer and a local folder that is not there. The block below is a container running the .NET SDK 10.0.401 with a single package source pointed at a server that answers 503 to everything. This page carries no reproduction on a Latchkey runner, so the host in the message is ours; the two line structure is the part that matches yours.
/w/app.csproj : error NU1301: Unable to load the service index for source http://mirror503:8899/v3/index.json.
/w/app.csproj : error NU1301: Response status code does not indicate success: 503 (Service Unavailable).
Failed to restore /w/app.csproj (in 5.44 sec).The message is a template, and the source is the variable
The first line is the same every time. In NuGet.Client the resource Log_FailedToReadServiceIndex is defined as "Unable to load the service index for source {0}.", and the only thing that changes is the source. So the first line tells you which feed, and nothing else. Everything diagnostic is on the second line, where the transport or authentication failure is printed verbatim.
That structure explains a confusion worth naming. The Microsoft documentation page for NU1301 illustrates the error with a local path, "The local source 'C:\Code\Contoso\contosoLocalSource' doesn't exist.", because a missing folder on disk is also a source that cannot be read. A CI failure is almost never that one, but a search for the code lands on that example and sends people looking at their filesystem.
| Second line says | What happened | Where the fix is |
|---|---|---|
| Response status code does not indicate success: 503 | The feed answered and refused. | Retry settings, and whether the feed belongs in this restore at all. |
| Response status code does not indicate success: 401 | The feed wants credentials the restore did not send. | The credential provider or a <packageSourceCredentials> entry. |
| A name resolution or connection failure | The runner never reached the host. | Egress, DNS, and any proxy the runner is behind. |
| The local source ... does not exist | A folder source is configured and is not on this machine. | The nuget.config that is in scope for the runner. |
Common causes
A private feed is down, throttled or slow
Azure Artifacts, a GitHub Packages feed or a self hosted Nexus answering 503 or 429 is the everyday version. Restore contacts the index before it knows what it needs, so the failure arrives before any package name appears and looks unrelated to the change that triggered the build.
The restore is authenticating to a feed it was never given credentials for
A private feed answers 401 to an anonymous index request, and NuGet reports that as NU1301 rather than as an authentication error, which sends people looking for an outage. On GitHub Actions this usually means the credential provider was not installed, or the source was added without a token in the step that added it.
A source from a developer machine is in scope on the runner
A nuget.config checked into the repository, or one in a parent directory of the checkout, can carry a local folder source or an internal host that does not exist in CI. NuGet merges configuration files up the directory tree, so a source nobody remembers adding is still contacted on every restore.
The runner cannot reach the host at all
A self hosted runner behind a proxy, a network policy that blocks egress to a new domain, or DNS that does not resolve an internal name. The second line says so plainly in this case, naming a connection or name resolution failure rather than a status code, which is the fastest way to tell this cause apart from the others.
How to fix it
Find out which source failed, and whether you need it
- Print the sources the runner will actually use, which is the merged result of every
nuget.configin scope, not just the one in the repository. - Remove sources the build does not need. Every extra feed is another chance for restore to fail on something irrelevant.
- Start the source list from a clean slate in the repository so a machine level configuration cannot add to it.
dotnet nuget list sourcePin the source list in the repository with a clear element
A nuget.config at the repository root that begins by clearing inherited sources makes the restore reproducible: the runner uses exactly the feeds listed and nothing a machine level file added. This alone removes the third cause above permanently.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="house" value="https://pkgs.dev.azure.com/org/_packaging/house/nuget/v3/index.json" />
</packageSources>
</configuration>Give the private feed credentials before restore runs
Add the source with a token in the same step that adds it, or install the artifacts credential provider. Doing it as part of the workflow rather than in a committed file keeps the token in a secret and keeps the source list honest about which feeds need authentication.
- run: >-
dotnet nuget add source
"https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
--name github --username ${{ github.actor }}
--password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-textShorten the retry ladder instead of adding one of your own
Restore already retries each source several times. When a feed is genuinely down you want to know in seconds, not after a compound of two retry loops. Lower the enhanced try count in CI and let the job fail fast, then re-run the workflow if you believe it was transient.
- run: dotnet restore --verbosity normal
env:
NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT: "3"NuGet does retry, and it retries more than you would guess
The 503 run above took 5.44 seconds and looked like a single attempt. It was not. Counting requests at the server, six GETs for /v3/index.json arrived before restore gave up. That is the documented default landing on the nose: NuGet describes NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT as "Configures the maximum number of times an HTTP connection should be retried when enhanced retries are enabled" and gives it a default of 6, with NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS defaulting to 1000. The sentence says "retried" and the variable says TRY_COUNT, and those two readings predict different numbers: seven requests if six retries sit on top of a first try, six if six is the whole budget. Six arrived, so it is the budget.
The practical consequence is that adding your own retry loop around dotnet restore multiplies a number that is already large. If restore is slow to fail in your pipeline, the fix is usually to reduce the number of sources it has to contact, not to raise a retry count.
Two more documented settings sit next to those, and they are easy to run together. NUGET_RETRY_HTTP_429 decides whether a 429 or a 408 is retried at all; its default is true, and setting it to false opts in to the behavior from before NuGet 6.5, which did not retry them. Honoring a Retry-After header is a separate switch, NUGET_OBSERVE_RETRY_AFTER, on by default, with NUGET_MAX_RETRY_AFTER_DELAY_SECONDS capping how long that header can make you wait. On a shared or throttled feed, leaving all three alone is the right call.
env:
NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT: "3"
NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS: "2000"Why this page has no runner reproduction
To show NU1301 you need a source that fails in a chosen way, because the second line is what this page is about. Cut a runner's egress and that line reads a name resolution or connection failure, which is a different row in the table above and a different fix. A source we control can be made to answer 503 on demand, so the line under the error carries the status we meant to demonstrate rather than whatever the network did on the day.
This slug has no entry in content/heal-evidence.mjs, so the page sets no healable flag, names no detection pattern and makes no claim that Latchkey repairs a restore that cannot reach its feed. The retry knobs above are NuGet's own and work on any runner.
How to prevent it
- Commit a
nuget.configthat starts with<clear />so the runner cannot inherit a source from anywhere else. - Keep the source list to the feeds the build needs, and remove one whenever a package moves.
- Add private feeds with credentials inside the workflow, never as a committed source entry that only works on a laptop.
- Cache
~/.nuget/packagesso a healthy restore does not depend on every feed answering on every run.
Frequently asked questions
Does NU1301 always mean the NuGet feed is down?
Why does dotnet restore fail when only one of my sources is broken?
How many times does NuGet retry the service index request?
NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT with a default of 6 and a retry delay default of 1000 milliseconds. Against a source answering 503 we counted six index requests arriving before restore gave up. The name is a try count rather than a retry count, so six is the whole budget and not a first attempt plus six, and wrapping restore in another retry loop compounds a ladder that is already long.