<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>sade.dev — Upgrading in Production</title><description>Treating runtime, framework and dependency upgrades as a risk decision rather than a technical chore: triggers, inventory, decision matrix, target version, prerequisites, rollout and the decision not to upgrade.</description><link>https://sade.dev/en/series/production-upgrade/</link><language>en-US</language><atom:link href="https://sade.dev/en/series/production-upgrade/rss.xml" rel="self" type="application/rss+xml"/><item><title>The Cost of Deferring an Upgrade</title><link>https://sade.dev/en/journal/the-cost-of-deferring-an-upgrade/</link><guid isPermaLink="true">https://sade.dev/en/journal/the-cost-of-deferring-an-upgrade/</guid><description>A PHP 7.1 → 8.2 upgrade was put off because &quot;it works.&quot; The bill came later: a dependency lock, batched breaking changes, and bugs spread over months.</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I proposed moving from PHP 7.1 to a stable 8.2, the answer was short: “It works, so why bother?” The answer was right — nothing was broken that day. What had started to break was invisible: packages were dropping support for old PHP one by one, and the application was getting locked to 7.1.&lt;/p&gt;
&lt;p&gt;“It works” describes today. The bill is issued by the days that follow.&lt;/p&gt;
&lt;p&gt;The objection isn’t illogical, so I don’t wave it away. Spending time on something that isn’t broken looks like gaining nothing. You can’t refute a sentence that is right by arguing with the sentence; you have to come with a number. What follows is the accounting of what that sentence cost me: by which mechanism, in which line items, and how much of it I was able to measure.&lt;/p&gt;
&lt;h2 id=&quot;how-the-lock-gets-set&quot;&gt;How the Lock Gets Set&lt;/h2&gt;
&lt;p&gt;The lock wasn’t set in a single day; with every package update, one more door closed.&lt;/p&gt;
&lt;p&gt;The mechanism is ordinary. A package drops support for the old runtime in its new version. Since the runtime hasn’t been upgraded, that version can’t be installed, and you stay on the old one. The same thing happens with the next package, then the one after. None of them raises an alarm on its own: the old version keeps working.&lt;/p&gt;
&lt;p&gt;At some point no package is left that can be updated. I call this a &lt;strong&gt;dependency lock&lt;/strong&gt;: the application is tied to the runtime, the packages to the application, and all of it together to 7.1. The way out is a single piece. I couldn’t move any package forward without upgrading the runtime; and without moving the packages forward, I couldn’t see what the upgrade would cost.&lt;/p&gt;
&lt;p&gt;That’s why the lock is set unnoticed. Each door that closes is, on its own, a detail that can be deferred; no single package update looks big enough to make you say “we have to do the upgrade today.” Deferring the decision is reasonable every single time. The sum of reasonable decisions is being backed into a corner.&lt;/p&gt;
&lt;p&gt;The worst thing about the lock is that it is silent. No error message says “another door closed today.” You notice it only on the day you try to update a package, while reading why the update won’t install.&lt;/p&gt;
&lt;p&gt;The sign to look for in your own system is simple. Of the pending package updates, how many can’t be installed because of the runtime version? Every number above zero shows that a door has closed.&lt;/p&gt;
&lt;h2 id=&quot;the-only-number-i-have&quot;&gt;The Only Number I Have&lt;/h2&gt;
&lt;p&gt;On the day I proposed it, the job was a 1–2 day upgrade. While the lock was being set, each package update took a few hours. Both looked like small jobs, and that’s why deferring was easy.&lt;/p&gt;
&lt;p&gt;A small job is a good candidate for deferral; “let’s finish the urgent thing first” fits into every calendar.&lt;/p&gt;
&lt;p&gt;Once deferred, the job didn’t just grow; because it had piled up, it became a single piece. I had to skip versions: not from 7.1 to 8.2 step by step, but passing everything in between in one go. Dozens of breaking changes and deprecated methods lay ahead of me, and all of them had to be solved at the same time. A debt that could have been paid in installments turned into a single collection.&lt;/p&gt;
&lt;p&gt;What the number really taught me isn’t the amount itself but the change in the kind of work. A few hours of package updates were piecemeal jobs that could be done in any order, on any day. Dozens of breaking changes are one indivisible job that has to be finished once started. Deferring didn’t only make the job bigger; it took away its flexibility.&lt;/p&gt;
&lt;p&gt;This is the only number I have: a 1–2 day job, plus a few hours per package update. I have no measurement to set next to it for the total cost of deferring. I won’t set one, either; writing down a number I didn’t measure would be a worse argument than “it works, doesn’t it.”&lt;/p&gt;
&lt;p&gt;Even so, this one number is enough to start a conversation. Against “it works, doesn’t it,” you can go not with “we’re gaining nothing,” but with “1–2 days now, dozens of changes in one go later.” To explain the difference between the two to a manager, a measurement isn’t essential; the direction of the comparison is enough.&lt;/p&gt;
&lt;p&gt;How to produce the number is in part 7 of the series; which upgrade comes first is in part 3. Here I’ll say only this: if I could have put this comparison on the table that day, “it works” alone wouldn’t have been enough of an answer.&lt;/p&gt;
&lt;h2 id=&quot;the-real-line-item-on-the-bill&quot;&gt;The Real Line Item on the Bill&lt;/h2&gt;
&lt;p&gt;We cleaned up the breaking changes; what we didn’t expect was the bugs that came after.&lt;/p&gt;
&lt;p&gt;Breaking changes have a good property: the error message points to them. You draw up the list, close them one by one, and it’s over. This item is long but known: you can roughly estimate its size, place it on the calendar, and tell your manager.&lt;/p&gt;
&lt;p&gt;The bugs weren’t like that. Some of them showed up in only a single case — a path that rarely gets triggered, a particular piece of data, a particular order. For a bug like that you first have to see it, then reproduce it, then find its cause. Each one is a separate investigation. Their fixes spread over months. One bug would close, and another would show up in a different case.&lt;/p&gt;
&lt;p&gt;For planning, the two have to be written separately. Breaking changes are a predictable item; bugs are not. If you build the budget only for the first, the second lives outside the budget and inside the calendar.&lt;/p&gt;
&lt;p&gt;That’s where the real cost is: the &lt;strong&gt;long tail&lt;/strong&gt;. The price of the batch migration fits the calendar; the price of the tail doesn’t. On top of that, because I was carrying dozens of changes together, the source of every bug was one of dozens of candidates; telling which bug came from which change created a job of its own.&lt;/p&gt;
&lt;h2 id=&quot;it-works-describes-today&quot;&gt;”It Works” Describes Today&lt;/h2&gt;
&lt;p&gt;Saying “there’s technical debt” isn’t enough against the “it works, doesn’t it” objection; the person across the table hears it as an opinion. To answer in business language, you have to break the risk into line items. Every deferred upgrade accumulates four:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;End of support:&lt;/strong&gt; once the runtime or framework is no longer maintained, no new fixes arrive. A set day on the calendar, a known date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time exposed to CVEs:&lt;/strong&gt; for every CVE announced after the day support ends, no official patch arrives; unless your distribution or a paid extended support patches it, the hole stays in the system until you upgrade. The counter starts the day support ends and stops on the day of the upgrade.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency lock:&lt;/strong&gt; as packages are pinned to old versions, your update options narrow. Once the lock is set, the only way out is to move everything together.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Growing version gap:&lt;/strong&gt; as the number of versions in between grows, so does the number of breaking changes and deprecated methods to solve in one go. In my case, that was dozens of items.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these produces an error today, and none shows on the monitoring dashboard; all of them will be paid tomorrow. For a manager the difference is this too: asking for budget for something broken is easy, explaining why something that isn’t broken will break is hard. That’s why naming the items works, because each one can be tied to a date, a counter, or a number.&lt;/p&gt;
&lt;p&gt;The summary of the four items: &lt;strong&gt;“it works” describes today; risk is tomorrow’s balance-sheet item.&lt;/strong&gt; The cost of upgrading is known, and you schedule the payment. The cost of deferring is unknown, and you don’t get to pick when it is collected.&lt;/p&gt;
&lt;aside class=&quot;tan-note&quot; aria-label=&quot;Tan says&quot; data-astro-cid-6xbnywkz&gt;&lt;p class=&quot;tan-note-head&quot; data-astro-cid-6xbnywkz&gt;&lt;img src=&quot;https://sade.dev/_astro/head-icon.BAMJxdJq_ZnUyWL.webp&quot; srcset=&quot;https://sade.dev/_astro/head-icon.BAMJxdJq_ZnUyWL.webp 1x, https://sade.dev/_astro/head-icon.BAMJxdJq_Zgjk1k.webp 2x&quot; alt loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-6xbnywkz=&quot;true&quot; width=&quot;32&quot; height=&quot;32&quot; class=&quot;tan-note-icon&quot;&gt;&lt;span class=&quot;tan-note-label&quot; data-astro-cid-6xbnywkz&gt;Tan says&lt;/span&gt;&lt;/p&gt;&lt;p class=&quot;tan-note-body&quot; data-astro-cid-6xbnywkz&gt;The post makes the cost of waiting plain. What bothers me is that the burden of proof always sits with whoever proposes the upgrade. People ask for a reason to upgrade; almost nobody asks for a reason to wait. If both questions were on the table, would “it works” still win so easily?&lt;/p&gt;&lt;/aside&gt;
&lt;h2 id=&quot;when-do-you-let-it-go&quot;&gt;When Do You Let It Go?&lt;/h2&gt;
&lt;p&gt;Not upgrading can also be a legitimate decision; in which cases, I’m leaving to &lt;span data-scheduled=&quot;/en/journal/deciding-not-to-upgrade/&quot;&gt;part 8&lt;/span&gt; of the series. My objection here isn’t to deferring, but to the deferral not being written down as a decision.&lt;/p&gt;
&lt;p&gt;Deferring can be a conscious decision; but a decision that isn’t written down comes back one day as a surprise. A deferral with its date and reason written down is risk management; one without is just forgetting.&lt;/p&gt;
&lt;p&gt;There is one thing you can do today: look up the end-of-support dates of the runtime and framework versions you use, and write them down somewhere. Even if the system has no inventory, two lines are enough: what’s running, and when its support ends. The next time someone asks “why?”, you’ll at least have a date in hand. If you also write next to the date how many versions behind the system will be by then, the question stops being “does it work” and becomes “when will we pay”; and the conversation starts there.&lt;/p&gt;
&lt;p&gt;Ask yourself: if upgrading this system today were a 1–2 day job, would you do it? If the answer is yes, there’s nothing to gain by waiting; the job only grows.&lt;/p&gt;
&lt;p&gt;The series continues from here: in part 1, the situations that trigger an upgrade; in part 2, the inventory and layers for seeing what will be upgraded; in part 3, the decision matrix; and up to part 8, the target version, prerequisites, rollout, and organization.&lt;/p&gt;
&lt;p&gt;When you defer an upgrade, you don’t close the risk; you write the bill to the next day.&lt;/p&gt;</content:encoded><category>technical-debt</category><category>decisions</category><category>dependencies</category><category>opinion</category><category>Journal</category><author>info@muhammetsafak.com (Muhammet Şafak)</author></item></channel></rss>