# nu1301 unable to load service index, and which source failed

> A nu1301 unable to load service index error means restore never read a feed. Find which source failed, then fix the retry and the source list.

Source: https://latchkey.dev/learn/failures/nuget-nu1301-service-index-unreachable-in-ci  
Updated: 2026-09-20

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.

```.NET SDK 10.0.401, package source answering 503
/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).
```

## 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

1. Print the sources the runner will actually use, which is the merged result of every `nuget.config` in scope, not just the one in the repository.
2. Remove sources the build does not need. Every extra feed is another chance for restore to fail on something irrelevant.
3. Start the source list from a clean slate in the repository so a machine level configuration cannot add to it.

```Terminal
dotnet nuget list source
```

### Pin 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.

```nuget.config
<?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.

```.github/workflows/ci.yml
- 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-text
```

### Shorten 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.

```.github/workflows/ci.yml
- run: dotnet restore --verbosity normal
  env:
    NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT: "3"
```

## How to prevent it

- Commit a `nuget.config` that 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/packages` so a healthy restore does not depend on every feed answering on every run.

## 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. |

> Restore reads every configured source, so one dead feed fails the whole restore even when every package you need lives on another one. That is the single most common shape of this failure in CI.

## 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.

```.github/workflows/ci.yml
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.

## FAQ

### Does NU1301 always mean the NuGet feed is down?

No. NU1301 is raised whenever a source cannot be read, which includes a 401 from a private feed, a name that does not resolve, and a local folder source that does not exist on the runner. The second NU1301 line carries the real reason, and it is the line to read before concluding anything about availability.

### Why does dotnet restore fail when only one of my sources is broken?

Because restore reads the service index of every configured source before resolving anything. A feed you do not need for this build still has to answer. Clearing inherited sources and listing only the feeds the project uses is the durable fix, and it also makes restore faster.

### How many times does NuGet retry the service index request?

More than once, and more than most people expect. NuGet documents `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.

### Should I set an HTTP timeout environment variable for NuGet restore?

There is no documented NuGet environment variable for an HTTP timeout, so a value copied from a blog post will simply be ignored. The documented network knobs are the enhanced retry count, the retry delay, the 429 behavior and the Retry-After cap. Use those and reduce the number of sources instead.

## References

- [Microsoft Learn: NuGet Error NU1301](https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1301)
- [Microsoft Learn: NuGet environment variables, including the enhanced retry settings](https://learn.microsoft.com/en-us/nuget/reference/cli-reference/cli-ref-environment-variables)
- [NuGet.Client: Log_FailedToReadServiceIndex in NuGet.Protocol Strings.resx](https://github.com/NuGet/NuGet.Client/blob/v11.0.0/src/NuGet.Core/NuGet.Protocol/Strings.resx)
- [Microsoft Learn: nuget.config reference and packageSources](https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
