Skip to content
Latchkey LogoLatchkey home

npm ERR! network errors in GitHub Actions

An npm ERR! network line in GitHub Actions means npm reached the registry and the request failed, not that anything is wrong with your dependency tree. Read the code printed under it: a 5xx, a throttle or an E-code is transient and worth retrying, while a 404 or an unsupported protocol will fail the same way every time.

Runner log: npm failing on a 503 from the registry, then the retry installing cleanly
The recorded run: npm exhausts its own retries against a registry answering 503, the runner retries the step, and the second attempt installs.
Diagram of which npm network codes are transient, the fix, and the runner action
The word network is the same on every one of these. The code under it is what decides whether a retry is the fix or a waste of two minutes.

What this error means

The install step fails and the log is a block of lines that all carry the same prefix, which is why people read the first one and stop. The useful line is the one with a number in it: "npm ERR! network 503 Service Unavailable - GET https://registry.npmjs.org/react" on npm 9 and earlier, or the same wording behind an "npm error" prefix on npm 10 and later. Under it npm prints a code, and the code is the whole diagnosis. 500, 502, 503 and 504 are the registry failing. A throttle is the registry telling you to slow down. The E-codes are the connection dying before an answer arrived. A 404 in the same shape is not a network failure at all, and neither is an unsupported protocol. The run below points npm at a stub registry on loopback rather than at registry.npmjs.org, because a 503 from the real one cannot be produced on demand; the error line is npm's own, printed after its built-in retries were exhausted.

Actions log, install step
npm error code E503
npm error 503 Service Unavailable - GET http://127.0.0.1:8899/lodash - Service Unavailable
npm error A complete log of this run can be found in: /home/runner/.npm/_logs/2026-09-20T08_09_38_389Z-debug-0.log

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 0·Repaired by NPM_NETWORK_REGISTRY_5XX

attempt 1, npm 10.8.2, stub registry on 127.0.0.1:8899 in fail mode
npm error code E503
npm error 503 Service Unavailable - GET http://127.0.0.1:8899/lodash - Service Unavailable
npm error A complete log of this run can be found in: /home/runner/.npm/_logs/2026-09-20T08_09_38_389Z-debug-0.log
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)
attempt 2, npm 10.8.2, stub registry on 127.0.0.1:8899 in recover mode

added 1 package in 669ms
installed lodash 4.17.21

Retried the npm command with exponential backoff after a registry 5xx response

The code under the line is the diagnosis

Every failure below prints the same word, and they do not have the same fix. Sort yours before you change anything, because four of these clear on a retry and two of them never will.

CodeWhat happenedWorth retrying
500, 502, 503, 504The registry answered with an error of its ownYes
A 429 responseThe registry is throttling this clientYes, after a wait
ETIMEDOUT, ESOCKETTIMEDOUTThe connection never completedYes
ECONNRESETThe connection was dropped part way throughYes
EAI_AGAIN, ENOTFOUNDDNS did not answer, or answered no such nameOnly the first
A 404 responseThat package or that registry path does not existNo
EUNSUPPORTEDPROTOCOLThe manifest uses a scheme npm has no resolver forNo

Common causes

The registry answered with an error of its own

The ordinary case, and the reason the same commit passes twenty minutes later. A public registry serving the whole industry has bad minutes, and a 5xx during them is not information about your project. It is also the case a retry fixes outright, which is why it is worth separating from the rest before you spend an afternoon on lockfiles.

The retry you added is not wrapped around the npm that failed

This is the most common wasted fix on this failure. A retry action around a build step does not reach an install running inside a container build, and npm config set in an earlier step writes a file the build container never sees. In our experience a workflow that keeps failing after a retry was added is usually failing inside a Dockerfile, where none of the runner-level configuration exists.

The connection dropped part way through a download

A reset or a socket timeout mid-transfer is a different shape from a clean error response: the registry was fine and the path to it was not. Parallel installs make it likelier, because a job opening fifty connections through one shared egress address notices the first drop.

It is not a network failure at all

Two codes share the wording and none of the cure. A 404 means the package name, the version or the registry path is wrong, and retrying it is how a five minute problem becomes a twenty minute one. EUNSUPPORTEDPROTOCOL means the manifest uses a dependency scheme that belongs to a different package manager, and no amount of network configuration will teach npm to read it.

How to fix it

Raise the retry budget where every npm can see it

  1. Put the four npm_config_* values in the job's env: block, not in a run: step.
  2. Raise the minimum wait as well as the count, so the retries land after the blip instead of inside it.
  3. Leave the outer retry action off until you have done this; two retry layers on top of each other mostly buy billed minutes.
.github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      npm_config_fetch_retries: '5'
      npm_config_fetch_retry_mintimeout: '20000'
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci --no-audit --no-fund

Move the settings to where the failing install runs

If the error comes from inside a container build, the configuration has to be in the image. Set the values with ENV in the Dockerfile, or mount an .npmrc as a build secret when it also carries a token. A container: job is the same story with a different lever: its env: is the one that counts.

Dockerfile
# BuildKit, with the token kept out of the layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --no-audit --no-fund

Open fewer connections and keep the store

Lower the socket count when resets are frequent, and cache the package store so most of the install never touches the network. setup-node with cache: npm restores the store npm reads from, which is a much better lever than caching node_modules.

Terminal
npm config set maxsockets 8
# or per job
env:
  npm_config_maxsockets: '8'

Read the code before you retry anything

A 404 needs the name, the version or the registry URL corrected, and a private package needs a token that the runner has. An unsupported protocol needs the package manager the repository was written for: enable Corepack and run that one instead of npm.

Terminal
npm config get registry
corepack enable
pnpm install   # for a manifest using pnpm catalogs
yarn install   # for portal: or link: specifiers

npm already retried, which is why you are reading the error

npm does not give up on the first failed request. It retries twice by default, waiting 10 seconds and then up to a minute, and it gives each request 5 minutes before it calls it a timeout. The line in your log is what it prints once that budget is spent, which is what most workflows get wrong: an outer retry on top of a budget nobody raised just waits out the default attempts twice.

Raising the budget is one setting, and in CI it belongs in the environment rather than in a command you have to remember to repeat. Every npm config key has an environment variable, so one env: block covers every npm in the job.

.github/workflows/ci.yml
env:
  npm_config_fetch_retries: '5'
  npm_config_fetch_retry_mintimeout: '20000'
  npm_config_fetch_retry_maxtimeout: '120000'
  npm_config_fetch_timeout: '600000'

The retry has to reach the npm that actually failed

A step-level retry action wraps the command the workflow runs. It does nothing for an npm running three layers down: inside docker build, inside a container: job, inside a tool that shells out to npm for you. Those processes read their own environment and their own .npmrc, and they have never heard of the runner's.

The test is simple. If the failing line appears under a #N [builder N/M] RUN npm ci heading, the settings have to be in the image, not in the workflow, and a retry belongs around the build rather than around the install.

Dockerfile
# Dockerfile
ENV npm_config_fetch_retries=5 \
    npm_config_fetch_retry_mintimeout=20000
RUN npm ci --no-audit --no-fund

What the runner does about it

Latchkey carries three separate detections for this family rather than one, because the right wait is different for each. NPM_NETWORK_REGISTRY_5XX fires on a registry 5xx at confidence 0.95 and retries with a 2 second base backoff. NPM_NETWORK_TIMEOUT fires on the E-codes at 0.94 with the same short backoff. NPM_REGISTRY_RATE_LIMIT fires on a throttle at 0.94 and starts at 60 seconds instead, because, in the library's own words, "the limit recharges much slower than a 5xx blip".

The recorded run above is the first of those. npm exhausted its own retries against a registry answering 503, the runner retried the step, and the second attempt installed the package. The pattern library is explicit about why that line is safe to act on: a registry that fails twice and then succeeds leaves npm exiting 0 with no error token in either stream, so a 503 in the buffer means npm had already given up.

How to prevent it

  • Set the npm retry budget in the job environment once, rather than per command.
  • Cache the npm store with setup-node, so a registry blip cannot stop most installs.
  • Keep a private registry token in secrets, so a 401 never arrives dressed as a network error.
  • Pin the package manager with packageManager so the wrong tool never runs the install.

Frequently asked questions

Why does npm install fail in CI with no change to package.json?
Because the failure is in the path to the registry rather than in the manifest. The same lockfile resolves the same versions every time, so a run that fails today and passes tomorrow failed on a request, not on a dependency. Read the code in the error: a 5xx or an E-code is the network, a 404 is the manifest.
How many times does npm retry a failed registry request?
Twice by default, on top of the first attempt. It waits 10 seconds before the first retry and up to a minute before the second, and it allows each request 5 minutes overall. Those are the fetch-retries, fetch-retry-mintimeout, fetch-retry-maxtimeout and fetch-timeout defaults, and all four have environment variables.
Why does the npm network error only happen inside docker build?
Because the build container has its own environment, its own .npmrc and often its own DNS. None of the settings you applied on the runner reach it. Set the npm values with ENV in the Dockerfile, and check whether the build stage has a working resolver before assuming the registry is at fault.
Should I run a registry mirror to stop npm network errors?
It helps when the volume is yours: a proxy turns most installs into a local fetch and takes the shared egress address out of the picture. It is a bigger change than raising the retry budget, so do it when registry failures are frequent rather than after one red build.

Related guides

References

A registry 503 says nothing about your dependency tree. Latchkey backs off and retries the install mid-job. Start free → 30-day trial · No credit card