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.


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.
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.logReproduced on a Latchkey runner
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.21Retried 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.
| Code | What happened | Worth retrying |
|---|---|---|
| 500, 502, 503, 504 | The registry answered with an error of its own | Yes |
| A 429 response | The registry is throttling this client | Yes, after a wait |
ETIMEDOUT, ESOCKETTIMEDOUT | The connection never completed | Yes |
ECONNRESET | The connection was dropped part way through | Yes |
EAI_AGAIN, ENOTFOUND | DNS did not answer, or answered no such name | Only the first |
| A 404 response | That package or that registry path does not exist | No |
EUNSUPPORTEDPROTOCOL | The manifest uses a scheme npm has no resolver for | No |
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
- Put the four
npm_config_*values in the job'senv:block, not in arun:step. - Raise the minimum wait as well as the count, so the retries land after the blip instead of inside it.
- Leave the outer retry action off until you have done this; two retry layers on top of each other mostly buy billed minutes.
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-fundMove 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.
# BuildKit, with the token kept out of the layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --no-audit --no-fundOpen 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.
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.
npm config get registry
corepack enable
pnpm install # for a manifest using pnpm catalogs
yarn install # for portal: or link: specifiersnpm 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.
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
ENV npm_config_fetch_retries=5 \
npm_config_fetch_retry_mintimeout=20000
RUN npm ci --no-audit --no-fundWhat 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
packageManagerso the wrong tool never runs the install.
Frequently asked questions
Why does npm install fail in CI with no change to package.json?
How many times does npm retry a failed registry request?
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?
.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.