A 401 means your credentials are missing or invalid, a 403 means you were understood and refused, a 404 means the item was not found, a 429 means you sent too many requests, and a 503 means the service is unavailable. Only 429 and 503 are worth retrying.
The decision table
| Code | Meaning | Retry? | First thing to check |
|---|---|---|---|
| 401 | Lacks valid authentication credentials | No, until fixed | Is the token present, expired or revoked? |
| 403 | Server understood the request and refused it | No | Does this identity have permission for this package or scope? |
| 404 | Resource not found | Rarely | Is the name, version or registry URL right? |
| 429 | Too many requests in a given time | Yes, after waiting | Is a Retry-After header present? Is the egress IP shared? |
| 503 | Server not ready, often maintenance or overload | Yes, with backoff | Is there a status page notice? |
The definitions follow the MDN reference for each status code.
401 and 403 look alike and are not
MDN notes that a 401 is similar to a 403, except that a 403 is returned when the request has valid credentials but the identity lacks permission. In CI terms:
- 401 usually means a secret expired, was rotated, or never reached the job. Fork pull requests often do not receive secrets, so a 401 only on forks is a clue.
- 403 means the token works but does not cover the package. MDN says re-authenticating makes no difference, so do not keep retrying with the same identity. Widen scope on purpose, or fix the package name.
Token settings matter here. npm granular tokens can be limited to specific packages and scopes, set to read only, given an expiry, and restricted by IP range. Any of those can produce a 401 or 403 that looks like an outage. See npmrc scoped registry and token scoping.
404 is not always an outage
MDN says a 404 does not indicate whether the absence is temporary or permanent. For a registry that matters. A package may be unpublished, a version removed, a scope pointed at the wrong registry, or a private name requested from the public one. That last case is also how dependency confusion works, so treat an unexpected 404 on a private name as worth a look, not a retry.
429 and 503 and the Retry-After header
For 429, MDN says a Retry-After header may be included to say how long to wait. For 503 it can indicate how long the service is expected to be unavailable. The header can be a number of seconds or a date. MDN also notes that client support for it is inconsistent, so do not assume your installer honours it. If you script retries, read the header yourself and wait at least that long, with a little random jitter so a fleet does not return at the same second.
A short triage routine
- Read the status code in the log, not just "install failed".
- 401 or 403: check the token and the scope. Do not retry.
- 404: check names and registry URLs. Look for a removed version.
- 429: back off, spread jobs out, authenticate, or add a cache. See why CI jobs hit pull rate limits.
- 503: wait, check the status page, and switch to your mirror if you have one.
Where Safeguard fits
Knowing which dependency failed to resolve is easier when you already have an inventory of what you use. Safeguard's SBOM and dependency scanning gives you that list, and you can try a free scan at gold.safeguard.sh/scan.
Quick check
Should CI retry a 403?
No. The request was understood and refused. Repeating it unchanged fails the same way. Fix the permission or the package name.
Is a 503 always the registry's fault?
It means the server is not ready, often from maintenance or overload. An overload can be partly caused by your own fleet, so check your request volume too.
Sources
- MDN: 401 Unauthorized
- MDN: 403 Forbidden
- MDN: 404 Not Found
- MDN: 429 Too Many Requests
- MDN: 503 Service Unavailable
- MDN: Retry-After header
- About npm access tokens
Last reviewed: 19 October 2026.