Skip to content
Latchkey LogoLatchkey home

err_pnpm_fetch_503 and the rest of the pnpm fetch codes

The code err_pnpm_fetch_503 is pnpm telling you the registry answered your request and refused it, and the number on the end is the HTTP status, not a pnpm error catalog entry. That is why the family is open ended: any status pnpm does not like becomes a code with that status in the name, and the one you get determines whether retrying is sensible or pointless.

pnpm fetch codes split by status, the retry ladder that ran first, and the settings
The number in the code is the HTTP status. Everything useful follows from that: whether pnpm retried, whether retrying again would help, and which setting is the one to move.

What this error means

The install fails during resolution, before anything is linked, on one package. Older pnpm prints two warnings about retrying and then a line beginning with the code, the HTTP method, the URL, the reason phrase and the status. pnpm 12 prints the code on its own line and then a small tree under it. Either way the three facts are the same: which URL, which status, and which package. The blocks below come from containers running the released pnpm versions against a local server that answers 503 to everything. This page carries no reproduction on a Latchkey runner, so the host is ours rather than a registry's.

pnpm 10.18.3, registry answering 503
 WARN  GET http://mirror503:8899/is-odd error (503). Will retry in 10 seconds. 2 retries left.
 WARN  GET http://mirror503:8899/is-odd error (503). Will retry in 1 minute. 1 retries left.
 ERR_PNPM_FETCH_503  GET http://mirror503:8899/is-odd: Service Unavailable - 503

The code is the status, and the status decides everything

A 503 is the registry saying it cannot serve you right now. A 404 is the registry saying that package is not there. pnpm reports both the same way, with the status appended to the code, and the difference in what you should do could not be larger. The 404 form also prints a hint the 503 form does not, which is a good confirmation that you are reading the right one.

Here is the 404 against the real public registry, from the same container, for a package name that does not exist. Note the hint, note the line about the authorization header, which is pnpm telling you it asked anonymously, and note that there are no retry warnings above it, because there is nothing to retry.

pnpm 10.18.3, real registry, package that does not exist
 ERR_PNPM_FETCH_404  GET https://registry.npmjs.org/is-odd-zzz-nope: Not Found - 404

This error happened while installing a direct dependency of /w

is-odd-zzz-nope is not in the npm registry, or you have no permission to fetch it.

No authorization header was set for the request.
Code you gotWhat the registry didIs retrying useful
ERR_PNPM_FETCH_404Answered, and has no such package or version.No. Check the name, the version and your auth scope.
ERR_PNPM_FETCH_401 or 403Answered, and refused you.No. The token or the scope registry is wrong.
ERR_PNPM_FETCH_429Answered, and asked you to slow down.Yes, with fewer parallel requests.
ERR_PNPM_FETCH_503Answered, and is unavailable.Yes. pnpm already tried; a longer ladder may help.

Common causes

The registry or the proxy in front of it is unwell

A public registry incident, a rate limited internal mirror, or a pull through cache that is reindexing. This is the honest 503, and the thing to notice is that pnpm already waited over a minute before reporting it, so a job that failed quickly after a 503 probably failed on something else.

Too many requests at once from one job

pnpm chooses networkConcurrency for itself, somewhere between 16 and 64 depending on the worker count, and an internal registry sized for humans rather than for a matrix of twenty jobs will start shedding load. The tell is that the same install succeeds when run alone and fails when the matrix is full, and that the status is 429 or 503 rather than a timeout.

A scoped package is being asked of the wrong registry

When the scope is not mapped, the request goes to the default registry, which answers 404 or 401 entirely correctly for a package it has never heard of. The code will have 404 or 401 in it rather than 503, and no retry setting will change the answer, because the request is being sent to the wrong host.

The lockfile names a tarball URL nobody can serve any more

A lockfile that was written against an internal mirror carries that mirror in its resolution entries. Run it in a job that cannot reach the mirror and the fetch fails on a host you never configured. The URL in the error is the giveaway, because it will not be the registry you think you are using.

How to fix it

Read the URL and the status before changing any setting

  1. Take the URL out of the error. If it is not the registry you expect, the problem is configuration, not availability.
  2. Take the status out of the code. 404, 401 and 403 are answers, not outages, and no retry setting addresses them.
  3. Only when the status is 429 or 5xx is the retry ladder the right lever.
Terminal
pnpm config get registry
pnpm config get @your-scope:registry

Lengthen the ladder for a genuinely flaky registry

Raise the retry count rather than the timeouts. Three or four retries against a 10 second minimum and a 60 second cap gives a registry a couple of minutes to come back, which covers most short incidents without hiding a real one for the rest of the day.

pnpm-workspace.yaml
fetchRetries: 4
fetchRetryMintimeout: 10000
fetchRetryMaxtimeout: 60000

Turn the request volume down before you turn the retries up

If the status is 429 or your own mirror is the one shedding load, fewer concurrent requests helps where more retries do not. Lower the network concurrency in CI only, so local installs keep their speed and the matrix stops behaving like a load test. pnpm documents its own networkConcurrency as landing between 16 and 64, so eight is below anything it picks for itself and is a genuine reduction rather than the default written out again.

.github/workflows/ci.yml
- run: pnpm install --frozen-lockfile
  env:
    NPM_CONFIG_NETWORK_CONCURRENCY: "8"

Cache the pnpm store so most installs need no registry at all

The store is content addressed, so a warm store turns an install into a link step and removes the network from the critical path for everything already in it. This is the fix that reduces how often the other three matter.

.github/workflows/ci.yml
- uses: pnpm/action-setup@v4
- run: echo "STORE=$(pnpm store path --silent)" >> "$GITHUB_ENV"
- uses: actions/cache@v4
  with:
    path: ${{ env.STORE }}
    key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- run: pnpm install --frozen-lockfile

What pnpm already did before it printed that

By the time you see the error, pnpm has retried. Its documented defaults are fetchRetries of 2, a fetchRetryFactor of 10, a fetchRetryMintimeout of 10000 milliseconds and a fetchRetryMaxtimeout of 60000. Ten seconds, then a minute, then the error. That is exactly what the two warnings in the block above are saying, and it means an install against a flapping registry already spent seventy seconds trying before it failed.

pnpm 12 does the same thing more quietly. Counting requests at the server, both pnpm 12.5.1 and pnpm 10.18.3 sent three requests for the package before giving up, which is one attempt plus the two documented retries. The difference between the versions is the output, not the behavior, so a log with no retry warnings in it is not evidence that retrying was skipped.

pnpm 12.5.1, same registry, captured at an 80 column terminal
Error: ERR_PNPM_FETCH_503

  × installing dependencies
  ╰─▶ Failed to resolve dependency tree: GET http://mirror503:8899/is-odd:
      Service Unavailable - 503

The timeout setting changed meaning in pnpm 12.4.0

This is worth checking against your version before you tune anything. fetchTimeout defaults to 60000 milliseconds, and pnpm documents it now as how long a request may go without making progress, with the clock reset on every chunk received. The same documentation states that before v12.4.0 it bounded the whole request.

The practical difference is real. Under the old meaning, a large tarball on a slow link could exceed the timeout while data was still arriving, so teams raised the value to something enormous. Under the new meaning that is not necessary, and a large value only delays the moment a genuinely stalled request is cut off. If you inherited a fetchTimeout of several minutes from an older pipeline, it is probably doing nothing useful now.

SettingDefaultWhat it controls
fetchRetries2How many times to retry a failed registry fetch.
fetchRetryFactor10The exponential factor for retry backoff.
fetchRetryMintimeout10000The lower bound of the backoff, in milliseconds.
fetchRetryMaxtimeout60000The upper bound of the backoff, in milliseconds.
fetchTimeout60000How long a request may go without making progress, from v12.4.0.
networkConcurrency16 to 64How many registry requests are in flight at once, chosen from the worker count.

Why this page has no runner reproduction

The public npm registry is not something we are going to make return 503, and a page that waited for it to happen would never ship. The harder requirement was the comparison. Saying that pnpm 12's quieter log is not a shorter ladder only means something if both releases meet the same failure, on the same package, against a server that has not moved between them. A runner job installs one pnpm and produces one log. A container installs each version in turn against the same responder, which is the only arrangement in which the two outputs can be set side by side and the difference attributed to the output rather than to the run.

This slug has no record in content/heal-evidence.mjs, so the page carries no healable flag and claims no repair. The settings below are pnpm's own and behave identically wherever the job runs.

How to prevent it

  • Cache the pnpm store, keyed on the lockfile, in every workflow that installs.
  • Map every private scope to its registry explicitly so a 404 from the public registry can never be the first sign of a misconfiguration.
  • Keep fetchRetries at 3 or 4 in CI and leave the timeouts at their defaults unless you have measured a reason.
  • Lower network concurrency for jobs that hit an internal registry, before the registry starts lowering it for you.

Frequently asked questions

What does ERR_PNPM_FETCH_503 mean in a pnpm install?
It means the registry answered the request with HTTP 503, so it was reachable and declined to serve. The number in the code is the status. pnpm will already have retried twice with a backoff before printing it, so the registry had roughly seventy seconds to recover and did not.
Does pnpm 12 still retry failed registry requests?
Yes. Counting requests at a server that answers 503, pnpm 12.5.1 and pnpm 10.18.3 both sent three, which is one attempt and the two retries the documented default allows. What changed is the output: pnpm 12 does not print a warning line for each retry, so a shorter log is not a shorter ladder.
How do I increase the pnpm fetch retry count in CI?
Set fetchRetries in pnpm-workspace.yaml, or pass it through the matching environment variable in the workflow. Raising it to three or four is usually enough. Leave fetchRetryMintimeout and fetchRetryMaxtimeout at their defaults of 10 seconds and 60 seconds unless you have a measured reason to move them.
Why does my pnpm install fail with a URL I never configured?
Because the lockfile carries resolution URLs from wherever it was written. A lockfile generated against an internal mirror will keep asking for that mirror in every job, including jobs that cannot reach it. Regenerate the lockfile against the registry CI actually uses, or make the mirror reachable from CI.

Related guides

References

The number in the pnpm code is the registry's, not pnpm's. Latchkey repairs transient failures on the runner. Start free → 30-day trial · No credit card