TypeScript

npm install stuck on idealTree buildDeps: find the stall

Reproduced on npm 10.9.9 against a server that accepts the connection and never replies: what each log level shows, why fetch-timeout does not end the wait, and the one-line fix.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

npm install stops on idealTree buildDeps when the very first network request it makes, usually the metadata for one of your dependencies, never gets an answer. npm keeps waiting without printing anything at the default log level. Run the install again with --loglevel silly: the last line names the URL npm is waiting on. Test that host with curl --connect-timeout 15, then fix what sits between you and it. Common culprits are a stale https-proxy or registry line in an .npmrc, a private registry you can no longer reach, or a broken network path. Raising or lowering fetch-timeout does not end the wait when the server accepts the connection but never replies.

The symptom, as quoted in the npm/cli issue #3359 (open since June 2021, 54 reactions) from npm 7's progress bar:

text
[....................] \ idealTree.pql-box: still idealTree buildDeps

What npm is doing at that step#

idealTree is the phase where npm works out which package versions it will install. buildDeps is the part that fetches each dependency's metadata (its packument) from the registry, or the tarball of a git dependency. Nothing can be resolved until those answers arrive, so a request that never completes stalls the whole command right there. One commenter on the issue measured about seven minutes of silence before npm gave up when registry.npmjs.org could not be reached. Later comments report other ways into the same stall:

These are reports from individual users, not diagnoses from npm. What they share is that the network answer never reaches npm. That is the part you can check yourself.

Why the timeouts do not save you#

npm's config definitions set fetch-retries to 2 and fetch-timeout to 5 minutes. The 5-minute value is described there as "The maximum amount of time to wait for HTTP requests to complete." The HTTP agent npm uses, @npmcli/agent, does not apply it as a total limit. It maps the value to an idle timeout and sets its own connection timeout to zero. These are the defaults in its timeouts object, unchanged between the copy bundled with npm 10.9.9 and the current main branch:

text
      idle: opts.timeout ?? 0,
      connection: 0,
      response: 0,
      transfer: 0,

In agents.js the idle timer is only armed after the connection is established. For a TLS registry, that means once the TLS handshake has finished. A connection that is accepted and then left silent is never established in that sense, so that timer never starts. The fetch layer above it, minipass-fetch, starts its own request timer only when the request is handed a socket. In the reproduction below, neither ended the wait. A refused or reset connection is different: it ends in an error, and npm retries it.

Reproduce the stall#

This was reproduced on 20 October 2026 (UTC) in Docker Desktop with the node:22-slim image (Node 22.23.3, npm 10.9.9). A second container on a private network accepts TCP on port 443 and never sends a byte. registry.npmjs.org was pointed at it with --add-host, and the project had one dependency, is-number. With --loglevel silly, the output stops on the request for that package:

text
npm silly idealTree buildDeps
npm silly fetch manifest is-number@7.0.0
npm silly packumentCache full:https://registry.npmjs.org/is-number cache-miss
elapsed: 120s

The 120 seconds is when timeout killed npm, not an error. Two more runs show how long the wait lasts:

  • Default settings: an install was still silent after about 11 minutes.
  • With --fetch-timeout=10000 --fetch-retries=0: an install was still silent after 8 minutes 50 seconds.

Both runs were stopped by hand. The full output is in the repro log for this article.

The same hang appears without touching DNS when the project .npmrc sends traffic to a proxy that never answers. At --loglevel verbose, npm printed its log-file lines and then nothing for two minutes:

text
npm verbose logfile /tmp/empty-cache/_logs/2026-10-20T23_25_40_828Z-debug-0.log
elapsed: 120s

Fix#

  1. Rerun the install with the most detailed log level, and read the last lines it prints before going quiet:
bash
npm install --loglevel silly

A line ending in cache-miss with no response after it names the URL npm is waiting on. If the connection is being refused rather than ignored, --verbose already shows it, one line per attempt. This is from the same container, with the registry address refusing connections:

text
npm http fetch GET https://registry.npmjs.org/is-number attempt 1 failed with ECONNREFUSED
npm http fetch GET https://registry.npmjs.org/is-number attempt 2 failed with ECONNREFUSED

At the default log level, that same run printed nothing at all for 60 seconds.

  1. Test that host outside npm. curl's connect timeout covers the TLS handshake too, so a silent server makes it fail instead of hang:
bash
curl --connect-timeout 15 https://registry.npmjs.org

Against the silent server it stopped after 15 seconds with:

text
curl: (28) SSL connection timeout

A working registry answers with JSON at once. If curl also fails, the problem is the network path, not npm. That is the test suggested in the issue.

  1. Check where npm is actually sending its requests. A proxy or registry line from an old .npmrc, whether in the project, in your home directory or in CI, overrides what you think npm is using:
bash
npm config get registry
npm config get https-proxy
npm config get proxy

In the reproduction, npm config get https-proxy printed http://172.19.0.2:443, the silent server.

  1. Remove the line that points npm at a dead host. For a line in the project .npmrc:
bash
npm config delete https-proxy --location=project

If the line sits in ~/.npmrc or a CI-provided .npmrc, edit that file instead. If the registry itself is unreachable from this network, that is something to fix in the network, the VPN or the private registry, not in npm.

Verify the fix#

Run the install again. In the reproduction, after the proxy line was deleted, npm config get https-proxy printed null and the install finished straight away:

text
added 1 package, and audited 2 packages in 758ms

A completed install prints the added … packages summary. To see each request with its status and time, add --loglevel http. Every npm http fetch GET 200 line is a request that got an answer.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

The original case: a GitLab git dependency#

The issue's title describes a narrower case. In a project with nothing but a git+ssh dependency on GitLab, npm install took over 60 seconds on npm 7, against 2 to 3 seconds on npm 6. A maintainer traced it to npm first trying to download a tarball of the repository from GitLab. That request failed (the maintainer got a 503 from it with wget) and npm waited "too long before just falling back to cloning the repo". The proposed change to GitLab's URL template, hosted-git-info PR #109, was closed without being merged. npm treats a 5xx answer as retryable: its config definitions say it "will retry idempotent read requests to the registry in the case of network failures or 5xx HTTP errors", and the retry test in make-fetch-happen applies to any non-POST request with a 5xx status, whatever the host. The maintainer's comment does not break the delay down attempt by attempt, but this case is a server that answered with an error, not a silent one.

Running the issue's exact package.json (no lock file, empty cache) on npm 10.9.9 in node:22-bookworm on 20 October 2026 (UTC) no longer reproduces the delay. GitLab now answers the tarball request with a 404 straight away, and npm falls back:

text
npm http fetch GET 404 https://gitlab.com/klesun/npm-seven-issue-dep/repository/archive.tar.gz?ref=5f1b313978986974d4364585e6bd3689276f1697 537ms (cache skip)
 
added 1 package, and audited 2 packages in 6s

If a git dependency on a self-hosted GitLab is still slow, use --loglevel http to see what happens to the archive URL: either it fails with a 5xx, which npm retries before falling back to git, or it never answers at all, which is the silent stall described above. The log line tells you which of the two you are dealing with before you change anything.

Once the install gets past buildDeps#

Once the registry answers, the install moves on to resolving and writing the tree, and different errors take over. A peer range that cannot be satisfied stops it with ERESOLVE, as in this Firebase prerelease conflict. If an earlier sudo npm left root-owned files in ~/.npm, the next stop is EACCES on the cache folder. On npm 6, a dependency whose package.json carries a version such as "1" fails with Invalid version. The hang itself is never an npm resolution problem: it is a request that went out and got no reply. Find which request it is, and the fix is usually a single .npmrc line or a network path that needs repairing.

Further reading: Fix Invalid options object: allowedHosts[0] with CRA proxy.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote