Insights

The Hidden Cost of Keeping Up: Why Dependency Upgrades Are So Hard

Upgrading dependencies is risky and frustrating—complicated by breaking changes, weak tests, and transitive dependency nightmares.

Modern software is built on layers of dependencies - reusable libraries that make our lives easier by handling everything from HTTP requests to date formatting. Managing these dependencies is a necessary part of modern development, yet few developers enjoy it. It’s tedious, fragile, and often thankless. But as painful as it is, keeping dependencies updated is critical - not just for performance and maintainability, but for security.

When a vulnerability is found in a package you depend on, the only real fix is to upgrade. The option to “just patch the hole” - is rarely available or streamlined. Security fixes come packaged inside version upgrades. 

Take, for example, the following real-world example: the high-severity vulnerability CVE-2024-4367 in pdfjs-dist that might lead to arbitrary execution. Say you are running version 2.14.305 of that package - to fix it, you must move from version 2.14.305 all the way to 4.2.67 - a major leap with potentially major consequences. This is the reality: security fixes and technology upgrades are tightly bound.

Upgrades Are Fragile, Even Risky

Upgrading should be simple - update the version number, re-run your tests, and ship. But in practice, it rarely is. Breaking changes between versions are common, even within the same major version. The cost? You need to modify your own code, adapt to new APIs, and retest everything. Even then, subtle behavioral changes can slip through. Your tests might pass, but edge cases might break in production.

Take for example a routine run of npm audit on the following test project. It suggests dozens of upgrades - some across major versions, many with the disclaimer of a potential breaking change.

Accepting those upgrades blindly is asking for trouble. Features might be deprecated, functions removed, or behaviors altered. There’s no strict contract preventing a minor or patch update from changing something in an unexpected way. Stats confirm this too: even within a major version, breaking changes are not rare. This research found that 33% of Patch versions and 64% of Minor versions had at least one API affected by a breaking change.

And then there’s the issue of test coverage - or lack thereof. Open source projects vary widely in quality. Some, like Google’s Guava, ship with over a million tests. Others, like jshttp/cookie, have very limited test depth - not necessarily out of neglect, but because many OSS packages are maintained by a single person or a very small team. The result? When something breaks, it might not be caught immediately. The cookie project, for instance, has had to release a fix to a fix in quick succession - a clear sign of struggling with upgrade stability.

When a dependency has poor coverage, your upgrade might work - or it might trigger a cascade of small bugs and emergency patches. And this unpredictability makes upgrades harder to trust.

The Transitive Trap

It gets worse when you consider transitive dependencies - the ones your dependencies depend on. A typical project brings in hundreds, sometimes thousands of these. You might update one library, only to find that it pulls in a dozen others - each with their own quirks, changes, and risks.

Upgrades become unpredictable. One seemingly minor bump could introduce a necessity to upgrade deeper in the stack. Consider the recent Ingres-Nginx vulnerability - upgrading the ingress version, forced many to upgrade their datadog agent as well, due to breaking changes.

Another example, back to our test project -

in order to fix vulnerabilities in axios@0.26.1, you are dependent on the sendgrid maintainer upgrading their own library first. That upgrade might be delayed, or might never happen. Your fix is blocked by a complete stranger, somewhere else in the supply chain.

Fix time becomes a wildcard.

What Developers Fear - And Why

There’s a real fear around breaking production systems. You roll out an upgrade, and something fails. Now you need to revert - but by that point, you’ve likely built new code on top of the upgraded version. The rollback is no longer clean. It’s a messy, time-consuming, and stressful process. You actually might end up paying twice as much, without getting any actual progress.

All this is happening while teams are overloaded. Security tickets compete with features and customer issues for attention. There’s often no clear owner for dependency updates - developers treat open source like free candy, when in reality, it’s more like a free puppy. It needs care, attention, and regular maintenance.

Where We Are - And How the Burden Builds, Endlessly

The situation is becoming increasingly burdensome. Upgrade backlogs tend to grow faster than they’re resolved, and in many repositories, the average dependency is now more than three years old. Over time, without consistent attention, the weight of outdated dependencies becomes harder to manage.

Dependency management may not get easy, but with steady habits, the right tools, and a realistic mindset, it can become a workable part of development. It’s a challenge worth meeting.

Our Blog

More articles