Live · Mon, Sep 7, 2026 · 03:01 UTC Block 843,917 Fees 14 sat/vB Fear & Greed 72 · Greed
Newsletter Pro Terminal Sign in
ITop Field News.
Subscribe →
Live · 03:01 UTC Block 843,917 F&G 72
Software development Software development desk

Dependency management: why your lockfiles keep lying to you

Lockfiles promise reproducible builds, but most dev teams discover they've been wrong about their dependencies only when something breaks in production. Here's what's actually going wrong.

Close-up of ethernet cables connected to a network switch panel in a data center.

Photo by Sergei Starostin on Pexels

Dependency management sits at the heart of every software project, yet it's one of the least examined parts of the build pipeline. Most teams adopt a package manager, commit a lockfile, and assume the problem is solved. Then a CI build succeeds on Tuesday and fails on Thursday with identical code. Or a working local environment produces broken containers. Or a security audit reveals a transitive dependency three levels deep that nobody intentionally installed.

The lockfile is supposed to prevent all of this. Often it doesn't. Understanding why is what separates teams that ship predictably from those that spend Fridays chasing ghost failures.

What lockfiles actually guarantee (and what they don't)

A lockfile pins exact versions and, in modern tooling, cryptographic hashes of package contents. package-lock.json in npm, Pipfile.lock in Pipenv, poetry.lock in Poetry, go.sum in Go: each one records what was resolved at a particular moment in time. Run the install command against the lockfile and you get those exact packages. That part works.

The problem is everything the lockfile doesn't control. Registry availability is one. If a package is unpublished or a registry is temporarily unreachable, the install fails even though the lockfile is "correct". Mutable tags are another: Docker base images referenced by tag (rather than digest) can change under you without the lockfile noticing, because the lockfile tracks application dependencies, not the environment those dependencies run in.

Then there's the native code problem. Packages with C or Rust extensions compile against the host system at install time. The lockfile records the package version, not the compiled artefact. Two developers with the same lockfile but different GCC versions or glibc releases can end up with binaries that behave differently. This is not hypothetical. It surfaces constantly in teams mixing macOS and Linux development environments.

Phantom dependencies and the resolution graph

Every package you install brings its own dependency tree. A typical Node.js project with 30 direct dependencies can have 400 or more packages in node_modules. Most of those are transitive: you didn't ask for them, but your direct dependencies did.

Phantom dependencies are the packages your code actually imports but hasn't declared in package.json. They exist in node_modules because something else depends on them. The lockfile pins their version. But if you update a direct dependency that no longer needs that transitive package, the phantom import breaks. Your code hasn't changed. Your lockfile has.

This is particularly acute in JavaScript because of flat module resolution. Python is somewhat better insulated by virtualenv isolation, but phantom dependency problems appear there too, especially when teams rely on implicit transitive installs rather than explicit top-level declarations. Go handles it better by design: the module system distinguishes between direct and indirect dependencies in go.mod, making the graph more legible.

The fix isn't complicated. Audit your direct imports against your declared dependencies. Tools like eslint-plugin-import for JavaScript and pipdeptree for Python surface phantom dependency usage before it becomes a production incident.

Lockfile drift: slow and hard to notice

Lockfile drift happens when the file on disk diverges from what's actually installed across environments. It builds up gradually. A developer runs npm install somepackage, commits the updated lockfile, but a colleague pulls and doesn't reinstall. CI uses a cached node_modules that predates the lockfile change. The staging server was last provisioned six months ago and nobody has run a clean install since.

By the time something fails, the divergence can span dozens of packages. The symptom looks like a bug in application code. The cause is a version mismatch in a dependency nobody was tracking.

Three practices prevent this. First, use the install command that enforces the lockfile rather than updating it: npm ci instead of npm install, pip install --require-hashes -r requirements.txt instead of plain pip install. Second, never cache node_modules or equivalent across CI runs unless you also cache-bust on lockfile hash changes. Third, treat a lockfile change in a pull request like a code change: require an explicit review of what changed in the dependency graph, not just that the lockfile was committed.

If your team is also navigating how code changes flow through branches, the Git branching strategy you use shapes how easily lockfile changes get reviewed and merged without conflict. Trunk-based teams tend to catch lockfile drift sooner because divergent branches are shorter-lived.

Security exposure in the dependency graph

The transitive dependency problem isn't just a reliability concern. It's a significant security surface. The 2021 log4shell incident and the 2022 PyPI supply chain attacks both demonstrated that a vulnerability buried three levels deep in the dependency tree is indistinguishable from a first-party vulnerability once it's in production.

Most teams run npm audit or pip-audit as a checkbox. The output gets ignored until it's flagged in a security review. By then, the vulnerable package may have been in production for months.

A more useful posture: automate dependency vulnerability scanning in CI with a hard fail on high-severity findings, and separate that from the weekly "dependabot noise" that teams learn to dismiss. Renovate Bot and Dependabot both support auto-merge for minor and patch updates when tests pass. That keeps the dependency graph fresh enough that critical patches get applied within days, not quarters.

The Australian Signals Directorate's Essential Eight includes patching application dependencies under its patching mitigation strategy. If your organisation is working toward Essential Eight compliance, dependency update hygiene isn't optional. For a broader look at that framework, the Essential Eight maturity model guide covers the patching requirements in detail.

Monorepo vs polyrepo: a different set of dependency problems

How you structure repositories changes which dependency problems you see most often. In a polyrepo setup, each service owns its dependency graph. Updates don't propagate automatically. A shared internal library gets updated in one repo, and three downstream services keep running the old version for weeks. You discover the inconsistency when a bug is fixed in v2.1 and a customer reports the same bug in a service still running v1.9.

Monorepos centralise the dependency graph but introduce their own complexity. With a single package.json or workspace configuration, a dependency upgrade affects every package in the repository simultaneously. That's safer for consistency but harder to manage when a breaking change in a major version requires migration work across 15 packages at once.

Neither model eliminates dependency management problems. The right choice depends on team structure and release cadence. If you're weighing these options more broadly, the detailed breakdown of monorepo vs polyrepo trade-offs covers the full picture beyond just dependency concerns.

Private registries and what happens when they go down

Many Australian enterprises run a private package registry: Nexus, Artifactory, or AWS CodeArtifact. Private registries proxy public registries, enforce licence policies, and host internal packages. They also introduce a single point of failure that teams forget about until it fails.

When the private registry is unreachable, builds fail regardless of what the lockfile says. If the registry is a pure proxy with no local cache, an upstream npm outage becomes your outage too. If the cache exists but wasn't pre-populated for the packages a new project needs, the first build fails.

The mitigation is unsexy: test registry availability as a separate step in CI before the install step runs, maintain a populated cache for your critical package set, and document the fallback procedure for a registry outage. That last item almost never exists until it's needed.

Version constraints: the silent accumulation of outdated packages

Dependency management is partly a versioning philosophy problem. Broad constraints (^1.0.0, ~1.0, >=1.0) keep the lockfile from resolving to outdated packages over time, because a fresh install will pull the latest compatible version. Pinned exact versions (1.0.3) make builds maximally reproducible but mean you're permanently on 1.0.3 until someone manually bumps the version.

Neither is universally correct. The practical answer for most teams: use broad constraints in package.json or pyproject.toml, let the lockfile provide the pinning, and automate the process of periodically regenerating the lockfile with updated resolutions. That gives you reproducibility today and a mechanical path to staying current.

The worst outcome is pinned exact versions with no automation. That's how teams end up running three-year-old libraries because nobody had time to update them, right up until a CVE is published against one.

A realistic checklist

  • Use npm ci, pip install --require-hashes, or equivalent lockfile-enforcing commands in CI, not the regular install command.
  • Verify package integrity hashes where your toolchain supports it (npm and Cargo do this by default; pip requires explicit configuration).
  • Treat lockfile diffs in pull requests as reviewable changes, not noise to accept blindly.
  • Run dependency vulnerability scanning in CI with a hard fail on critical/high findings.
  • Automate patch and minor dependency updates with Renovate Bot or Dependabot, configured with auto-merge when tests pass.
  • Audit your imports against declared dependencies at least quarterly using tooling, not manual review.

Dependency management won't generate interesting architecture diagrams or make it into a sprint review presentation. But the teams that get it right ship with fewer production incidents, patch faster when vulnerabilities emerge, and spend less time debugging problems that have nothing to do with their own code.

→ The Confirmations · Daily newsletter

One email at 06:00 UTC. Six minutes. The only digest written for desks, not for retail.