---
title: "Why npm install Is Slow in CI | StarSling"
description: "Diagnose why npm install or npm ci is slow in GitHub Actions: missing caches, duplicate installs across jobs, and duplicated setup."
url: https://starsling.dev/github-actions/problems/slow-npm-install
canonicalUrl: https://starsling.dev/github-actions/problems/slow-npm-install
---

# npm install or npm ci taking a large slice of every run

[GitHub Actions](https://starsling.dev/github-actions) / [Problems](https://starsling.dev/github-actions/problems) / npm install or npm ci taking a large slice of every run

- [How StarSling works](https://starsling.dev/)

Diagnosis mode: static. Last updated: 2026-08-31

A slow npm install almost always means one of three things: the npm cache is never restored, the same install runs more than once across jobs in the workflow, or several jobs each pay for an identical setup preamble. Check the setup-node step, the job graph, and the workflow's own step timings in that order before changing anything.

## Table of contents

- [Symptoms](#symptoms)
- [How to diagnose it](#how-to-diagnose-it)
- [Likely causes](#likely-causes)
- [Hand it to an agent](#verify)
- [Related pages](#related-pages)
- [Related symptoms](#related-symptoms)
- [Sources](#sources)

<a id="symptoms"></a>

## Symptoms

In a GitHub Actions run, npm install or npm ci resolves the dependency tree and writes every package to node_modules on a runner that started empty seconds earlier. If nothing restored a prior run's npm cache, that resolution refetches every tarball from the registry instead of reading it from disk, and the install pays full network cost every single time. That cost then multiplies in two common ways: a workflow with several jobs, lint, test, build, e2e, that each independently check out the repo and reinstall the identical dependency tree, or a workflow whose jobs each carry their own copy of the same checkout-and-setup preamble instead of sharing one. The order below follows how often each cause actually accounts for the time: no cache first, because it is the single biggest lever and the easiest to confirm from the setup-node step alone; duplicate installs next, because splitting work across jobs is normal and only becomes waste when nothing hands the result forward; then duplicated setup, which wastes less per job but adds up across a wide job graph.

- The install step alone takes a minute or more on almost every run, even when the lockfile has not changed since the last commit.
- The workflow has several jobs, lint, test, build, e2e, and each one has its own npm install or npm ci step near the top.
- The setup-node step's log reads "npm cache is not found" on runs where the lockfile is identical to the previous run.
- Total workflow time barely moves even after the actual test or build commands got faster.
- Two or more jobs in the same workflow file open with the same sequence of checkout, setup-node, and install steps.

<a id="how-to-diagnose-it"></a>

## How to diagnose it

[ci-speedup](https://starsling.dev/ci-speedup) carries the detection logic behind the causes below and opens the fix as a reviewable pull request. Install it with `npx skills add starslingdev/skills`, then run `/ci-speedup` in your repository.

To check by hand:

1. Open the workflow file and list every job: `yq '.jobs | keys' .github/workflows/ci.yml`. Note which jobs contain an install step.
2. For each install step, check the preceding setup-node block for `cache: npm`. Its absence on any job is cause 1 for that job.
3. Open one recent run in the Actions UI and read the setup-node or cache step's log line: a cache hit or miss settles cause 1 versus cause 2 without guessing.
4. Count install steps across the whole workflow file and check for `actions/upload-artifact` / `actions/download-artifact` between them; more than one install with no handoff points at cause 3.
5. Compare the first four steps of every job with `yq '.jobs | to_entries[] | {name: .key, steps: .value.steps[0:4]}'`; identical sequences across jobs point at cause 4.
6. If the install step still dominates total job time after ruling out the above, compute the ratio of setup time to total job time directly from the run's step timings in the Actions UI to confirm how much of the run install actually accounts for.

<a id="likely-causes"></a>

## Likely causes

Ordered by how often each one turns out to be the answer. Confirm a cause with its check before you change anything.

### 1. npm's own cache is never restored

actions/setup-node has a built-in cache input that wraps actions/cache around npm's package cache, keyed on the lockfile, but it only activates when a workflow sets it. Without it, npm ci resolves against the lockfile correctly but has nothing local to read from, so every package is refetched over the network on every run. Separate downloads a job does outside npm's own cache, browser binaries, SDKs, are invisible to that flag and need their own cache step, and an install step for a tool the job never runs is pure waste layered on top of a cold cache.

Confirm it: `grep -A5 'actions/setup-node' .github/workflows/*.yml` and check whether the block includes a `cache: npm` (or `cache: 'npm'`) line; if it is absent, this is the cause.

Fix: [npm install caching](https://starsling.dev/github-actions/optimizations/optimize-npm-install) (`ci.cache.npm-install`, detection mode static)

### 2. The dependency cache exists but never hits

A workflow can have an actions/cache or setup-node cache step in place and still see no benefit if the cache key never matches: a key that is not derived from the lockfile hash, a key that includes something that changes every run (a timestamp, a commit SHA), or a restore-keys fallback that is too broad to be useful. In that state the workflow looks cached but behaves exactly like an uncached install.

Confirm it: Open a recent successful run's setup-node or cache step in the Actions UI and read the log line directly. Both actions print "Cache restored from key" on a hit, but they word the miss differently: actions/cache prints "Cache not found for input keys", while actions/setup-node prints "npm cache is not found" (with `pnpm` or `yarn` in place of `npm` for those managers). Match the string to the action the job actually uses, then check that the key in the workflow YAML includes `hashFiles('package-lock.json')` or an equivalent lockfile hash.

Fix: [Dependency caching](https://starsling.dev/best-practices/github-actions/cache-dependencies) (`ci.cache.dependency-cache`, detection mode static)

### 3. Multiple jobs each install the same dependencies from scratch

Every job in a GitHub Actions workflow gets its own fresh runner with nothing carried over from a sibling job, so a lint job, a test job, and a build job that each check out the repo and run npm install independently resolve and write the same dependency tree that many times. Nothing about running jobs in parallel requires this: it only happens when no job hands its installed dependencies to the others.

Confirm it: `grep -rn 'actions/upload-artifact\|actions/download-artifact' .github/workflows/` combined with counting install steps per workflow file; a workflow with two or more install steps and zero artifact-handoff matches is reinstalling from scratch in every job.

Fix: [Duplicate dependency installs](https://starsling.dev/github-actions/optimizations/avoid-duplicate-dependency-installs) (`ci.cache.duplicate-installs`, detection mode static)

### 4. The same checkout-and-install preamble is copy-pasted into every job

When several jobs in a workflow each carry their own copy of checkout, setup-node, and install steps instead of sharing a composite action or reusable workflow, the runner pays that preamble's cost once per job, and a version bump or cache-key fix made in one copy silently does not apply to the others until someone remembers to update every copy.

Confirm it: `yq '.jobs | to_entries[] | {name: .key, steps: .value.steps[0:4]}' .github/workflows/ci.yml` and compare the opening step sequence across jobs; identical sequences with no shared composite action behind them confirm this cause.

Fix: [Duplicated job setup](https://starsling.dev/github-actions/optimizations/avoid-duplicated-setup) (`ci.hygiene.duplicated-setup`, detection mode static)

<a id="verify"></a>

## Hand it to an agent

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this diagnosis against your repository and report which cause it found:

Diagnose why npm install or npm ci is slow in this repository's GitHub Actions workflows. Do not change anything yet; report findings first.

1. List every workflow under .github/workflows/ and every job in each, noting which jobs run npm install or npm ci.
2. For each such job, check whether its actions/setup-node step sets `cache: npm`. If it does not, that is likely cause 1.
3. If setup-node's cache is enabled, open the most recent successful run's log for that step and check whether it reports a cache restore or a cache miss. A miss despite the flag being set points at cause 2 (a bad or non-lockfile-derived cache key).
4. Count install steps across each workflow file. If more than one job installs the same dependencies with no actions/upload-artifact and actions/download-artifact handoff between them, that is cause 3.
5. Compare the opening steps (checkout, setup, install) across jobs in the same workflow. Identical sequences repeated with no shared composite action is cause 4.

Report which cause (or causes) you found, with the file and job names, before proposing or making any change.

<a id="related-pages"></a>

## Related pages

- [Optimize npm installs in GitHub Actions](https://starsling.dev/github-actions/optimizations/optimize-npm-install)
- [GitHub Actions cache: dependencies, keys, and cache hits](https://starsling.dev/best-practices/github-actions/cache-dependencies)
- [Stop reinstalling the same dependencies in GitHub Actions](https://starsling.dev/github-actions/optimizations/avoid-duplicate-dependency-installs)
- [Share setup steps across GitHub Actions jobs](https://starsling.dev/github-actions/optimizations/avoid-duplicated-setup)
- [Optimize pnpm installs in GitHub Actions](https://starsling.dev/github-actions/optimizations/optimize-pnpm-install)
- [Why GitHub Actions is slow, and how to fix it](https://starsling.dev/github-actions-too-slow)

<a id="related-symptoms"></a>

## Related symptoms

Other symptoms on this site that share a likely cause with this one. If none of the causes above is yours, one of these is usually the page you wanted.

- [pnpm install is slow in GitHub Actions](https://starsling.dev/github-actions/problems/slow-pnpm-install)
- [GitHub Actions cache reports a miss on every run](https://starsling.dev/github-actions/problems/github-actions-cache-not-working)
- [Why your test job is slower in CI than it is locally](https://starsling.dev/github-actions/problems/slow-tests)

<a id="sources"></a>

## Sources

- [actions/setup-node: caching global packages data](https://github.com/actions/setup-node#caching-global-packages-data)
- [GitHub Actions: caching dependencies to speed up workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows)
- [GitHub Actions: storing workflow data as artifacts](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts)
- [npm docs: npm ci](https://docs.npmjs.com/cli/commands/npm-ci)
