DEV Community

Nicholas Toledo
Nicholas Toledo

Posted on

Your EOL dependencies just became a legal question, not a tech-debt question

Every backlog has a ticket like this:

Upgrade Node 16 → 20. Priority: low. Nobody has complained.

On 8 December 2026 that ticket changes category. It stops being tech debt and starts being a civil liability question, at least for anything you place on the EU market.

This is not a scare post. The mechanism is narrow, specific and readable in about twenty minutes of primary source. Then the engineering problem — what, exactly, in my codebase can no longer receive a security update? — turns out to be a question you can answer with curl and jq today.

The legal change, in engineering terms

The relevant text is Directive (EU) 2024/2853, the revised Product Liability Directive. It replaces the 1985 directive. Three things matter to people who ship software.

1. Software is a product. Article 4(1):

'product' means all movables, even if integrated into, or inter-connected with, another movable or an immovable; it includes electricity, digital manufacturing files, raw materials and software

No qualifier about delivery mechanism. Embedded firmware, a downloaded binary, a SaaS backend — the recitals are explicit that the mode of supply does not change the answer.

2. Liability is no-fault. This is the part engineers usually get wrong. Strict liability does not mean "you were careless". A claimant does not have to prove negligence. They have to prove three things: the product was defective, they suffered damage, and the defect caused the damage. Your internal process quality, your code review standards, your intentions — none of that is an element of the claim.

Article 7(1) defines defective as not providing "the safety that a person is entitled to expect". Article 7(2) lists the circumstances a court weighs, and 7(2)(f) includes:

relevant product safety requirements, including safety-relevant cybersecurity requirements

3. "It was fine when we shipped it" got much weaker. Article 11(1)(c) is the classic later-defect defence: prove the defect came into being after you placed the product on the market and you are exempt. Article 11(2) then carves a hole straight through it:

By way of derogation from paragraph 1, point (c), an economic operator shall not be exempted from liability where the defectiveness of a product is due to any of the following, provided that it is within the manufacturer's control: (a) a related service; (b) software, including software updates or upgrades; (c) a lack of software updates or upgrades necessary to maintain safety; (d) a substantial modification of the product.

Read 11(2)(c) next to Recital 19, which says a product remains within the manufacturer's control "where the manufacturer retains the ability to supply software updates or upgrades itself or via a third party".

That is the whole chain. If you can still ship updates, the product is still yours. And a lack of the security updates needed to maintain safety is named, in the text, as something that does not get you out.

The scope limits nobody mentions

  • It applies to products placed on the market or put into service after 8 December 2026 (Article 2(1)). Note the date: Article 2(1) as first published said 9 December, and was corrected to 8 December by Corrigendum 2026/90364. Most commentary still says 9 December. Older products stay under the 1985 regime; it is not retroactive.
  • 9 December 2026 is the transposition deadline (Article 22) — one day after the scope date, which is why both dates appear in commentary. This is a directive, not a regulation. The text that binds you is your member state's implementing law, which may differ at the edges.
  • Non-commercial FOSS is out of scope. Article 2(2): the directive does not apply to "free and open-source software that is developed or supplied outside the course of a commercial activity". That is about how it is supplied, not the licence. Maintaining an MIT library as a hobby is excluded. Selling a product built on it is not.
  • Recoverable damage is death, personal injury, property damage and destruction or corruption of data — not pure economic loss. A CRUD app that annoys people is not the target. A product that can hurt someone or destroy their data is.

I am an engineer, not a lawyer. The above is me reading a primary source and quoting it, and none of it is legal advice. If you ship into the EU, take real advice on scope. The engineering question below stands regardless.

The engineering question

Of everything I ship, what can no longer receive a security update at all?

That is narrower than "what has a CVE". A component past its upstream end-of-life has no future patch, by definition. If a vulnerability lands in Node 16 tomorrow, there is no fix coming. Ever. "We'll patch it when it matters" is not available to you.

Good news: there is a free public catalogue of exactly this, and you do not need a tool to query it.

Doing it with curl

endoflife.date tracks EOL dates for runtimes, frameworks, databases and OSes. The v1 API is public, unauthenticated and rate-limit-friendly.

curl -s https://endoflife.date/api/v1/products/nodejs \
  | jq '.result.releases[] | select(.name=="16")
        | {name, releaseDate, isEol, eolFrom, latest: .latest.name}'
Enter fullscreen mode Exit fullscreen mode

Real output, today:

{
  "name": "16",
  "releaseDate": "2021-04-20",
  "isEol": true,
  "eolFrom": "2023-09-11",
  "latest": "16.20.2"
}
Enter fullscreen mode Exit fullscreen mode

Node 16 went EOL on 11 September 2023. The last release that will ever exist is 16.20.2.

The full response is {schema_version, generated_at, last_modified, result} where result has name, label, category, tags, identifiers, links and releases[]. Each release carries isEol / eolFrom, plus isEoas / eoasFrom for end-of-active-support (security fixes only after this) and isEoes / eoesFrom for extended support.

List every dead Node line:

curl -s https://endoflife.date/api/v1/products/nodejs \
  | jq -r '.result.releases[] | select(.isEol) | "\(.name)\teol \(.eolFrom)"'
Enter fullscreen mode Exit fullscreen mode
25  eol 2026-06-01
23  eol 2025-06-01
21  eol 2024-06-01
20  eol 2026-04-30
19  eol 2023-06-01
18  eol 2025-04-30
17  eol 2022-06-01
16  eol 2023-09-11
Enter fullscreen mode Exit fullscreen mode

Note Node 20 in that list. An LTS release that many people still think of as current went EOL on 30 April 2026 — which means the backlog ticket at the top of this post ("upgrade Node 16 → 20") would have migrated you onto another dead runtime. That is exactly the kind of thing you want a machine to check rather than a memory.

Same shape for anything else:

curl -s https://endoflife.date/api/v1/products/python \
  | jq '.result.releases[] | select(.name=="3.7") | {name, isEol, eolFrom}'
Enter fullscreen mode Exit fullscreen mode
{
  "name": "3.7",
  "isEol": true,
  "eolFrom": "2023-06-27"
}
Enter fullscreen mode Exit fullscreen mode

The catalogue currently covers 473 products (curl -s https://endoflife.date/api/v1/products | jq '.total'). There is also a legacy endpoint, https://endoflife.date/api/nodejs.json, which returns a flat array of cycles with cycle / eol / latest keys. It still works and you will see it in older scripts, but the v1 endpoint is the one to build on.

You now have everything you need to write this yourself: parse .nvmrc, go.mod, Dockerfile FROM lines and package.json engines, map each to a product slug, fetch, compare eolFrom to today. It is an afternoon of work, and if you have unusual manifests it is the right call.

Doing it with a tool

I wrote the afternoon-of-work version so I would stop re-writing it: pld-watch. Single Python file, standard library only, MIT, no account and no telemetry.

curl -fsSL https://raw.githubusercontent.com/ntoledo319/pld-watch/main/pld_watch.py -o pld-watch
chmod +x pld-watch
./pld-watch scan
Enter fullscreen mode Exit fullscreen mode

Against a deliberately awful test project (.nvmrc pinned to 16.20.2, FROM python:3.7-slim, next: ^12.0.0):

pld-watch v1.0.0
EU Product Liability Directive (EU) 2024/2853 - update-supportability screen

  Scope    /tmp/demoproj
  Found    6 runtime/framework version(s)
  PLD      applies from 2026-12-09 (86 days away)

  --------------------------------------------------------------------

  4 COMPONENT(S) PAST UPSTREAM END-OF-LIFE

    Node.js 16.20   (.nvmrc)
      upstream EOL   2023-09-11   latest supported: 26
    Node.js 16   (package.json engines.node)
      upstream EOL   2023-09-11   latest supported: 26
    next 12.0   (package.json)
      upstream EOL   2022-11-21   latest supported: 16
    python 3.7   (Dockerfile FROM)
      upstream EOL   2023-06-27   latest supported: 3.14
Enter fullscreen mode Exit fullscreen mode

It reads .nvmrc, .python-version, go.mod, package.json (engines plus a known-framework map), requirements*.txt and Dockerfile FROM lines. Exit codes are 0 nothing past EOL, 1 at least one past EOL, 2 nothing detectable to check. Responses are cached for 24 hours under $XDG_CACHE_HOME/pld-watch so CI does not hammer a free API.

What this does not do

Be clear-eyed about it, because a tool that oversells itself is worse than no tool.

It compares declared versions against a catalogue. That is one narrow checkable fact. It does not:

  • prove your product is defective — that is decided by a court on the facts, not by a scanner
  • prove the EOL component is reachable or exploitable in your build
  • tell you whether you are in scope of the directive
  • read binaries, container image layers or your own source
  • know any framework outside its slug map

The EOL date is a fact about upstream. Everything downstream of it is judgement. What the tool actually buys you is that the fact stops being invisible.

And there is a broader reason to run this that has nothing to do with Brussels: a component that cannot receive a security patch is a genuine engineering liability whether or not anyone ever sues you. The directive is just the thing that finally makes it someone else's problem too.

Where to start

  1. curl the endoflife.date API for your top three runtimes. Ten minutes.
  2. Write down which components cannot receive a patch, and what it would cost to move each one.
  3. Put the check in CI so the list cannot silently grow.

Step 3 is the one that matters. EOL dates arrive on a schedule you do not control — something you shipped clean today goes EOL in April without a single commit.


Tool: pld-watch. Engineering tooling, not legal advice, and not a compliance determination.

Top comments (0)