Preparing your learning space...
100% through Production & Reliability tutorials
Launch day isn't the finish line. A solution that runs for a customer for years needs care after it ships: someone to own it, a way to deal with the shortcuts taken to hit deadlines, dependencies that get updated, and documentation that doesn't rot. This tutorial covers the unglamorous but essential work of keeping a solution healthy over time — and knowing when it's time to retire one gracefully.
Long-term maintenance is everything you do to keep a solution working well after it ships: fixes, upgrades, documentation, monitoring, small improvements — all of it done on a rhythm, not in a panic.
Systems don't fail because of big disasters. They rot slowly. Dependencies go out of date. A bug lives unfixed for a year. The person who built it leaves. The docs describe a version that hasn't existed in six months. Maintenance is the discipline that stops that slow rot from turning into an outage — or a rewrite.
A maintained solution has an owner. It sounds obvious, but most abandoned systems were abandoned because nobody had explicit responsibility for keeping them alive after the initial launch hype faded.
An ownership card answers the questions people ask when something breaks at midnight:
Owner: ana@company.com Backup: dev@company.com Repo: github.com/company/expense-tool Runbook: docs/runbooks/expense-tool.md On-call rotation: weekly, ana then dev Change approval: PR + one reviewer
The owner doesn't do everything themselves. They make sure things happen: fixes get prioritized, the runbook stays current, dependency updates get scheduled. A named owner turns "someone should handle that" into "ana is handling that." That shift alone prevents most of the rot.
Technical debt is the cost of shortcuts taken to ship on time. Code you'd write differently if you had more time. Tests you skipped. A hacky integration that works but is brittle. It's not a sin — it's a trade you made to hit a deadline. The trouble starts when you forget you made it.
The fix is to make debt visible. Track it where people can see it:
debt_log = [
{"item": "replace cron polling with webhooks", "added": "2026-03-10",
"effort": "S", "risk": "M", "status": "backlog"},
{"item": "write tests for the invoice matcher", "added": "2026-05-02",
"effort": "M", "risk": "H", "status": "backlog"},
{"item": "remove duplicated export code (copied for Acme)", "added": "2026-06-18",
"effort": "S", "risk": "M", "status": "backlog"},
]
backlog = [d for d in debt_log if d["status"] == "backlog"]
high_risk = [d for d in backlog if d["risk"] == "H"]
print(f"backlog: {len(backlog)} items, {len(high_risk)} high risk")
A simple list like this makes the invisible visible: what's owed, how risky each item is, and whether anything has actually been paid off. From here, "paying down debt" becomes a habit. Always fix a little when you're already in the code touching related things, and schedule a slice of time each cycle for the highest-risk items. That's where the next outage is hiding.
Every solution stands on third-party code — libraries, packages, cloud services. That code keeps evolving, and old versions accumulate security holes and incompatibilities. Keeping dependencies current is the most predictable maintenance task, and also the one most often skipped until a vulnerability announcement forces a panicked upgrade.
First, know what you have:
pip list --outdated # Python packages with newer versions
npm outdated # Node packages with newer versions
Then update on a schedule rather than in a panic:
Weekly: check for known-vulnerability announcements Monthly: run the outdated report, review what changed Quarterly: upgrade in a branch, run the test suite, deploy to staging
Upgrading quarterly in small, tested batches beats upgrading once a year in one giant, terrifying leap — and it beats never upgrading until something forces your hand. Critical security patches skip the schedule and get bumped to "do it now."
Documentation has a shelf life. A README written at launch describes a system that has since changed, and the day it stops matching reality is the day people stop trusting it. Outdated docs are actually worse than no docs — they send the next engineer confidently in the wrong direction.
The fix isn't a big rewrite. It's a freshness habit. Put a "last reviewed" date on key docs and check them on a schedule:
## Runbook — Expense Tool Status: CURRENT Last reviewed: 2026-08-12 Owner: ana
import datetime
docs = {
"runbook.md": datetime.date(2026, 8, 12),
"architecture.md": datetime.date(2026, 1, 30),
"onboarding.md": datetime.date(2025, 11, 4),
}
stale = [d for d, reviewed in docs.items()
if (datetime.date.today() - reviewed).days > 180]
print("stale docs:", ", ".join(stale) or "none")
Any doc not reviewed in six months gets flagged. Read it, fix what's wrong, bump the date. Five minutes every quarter per doc keeps things roughly true — which is all anyone needs from a runbook.
Maintenance works best on a rhythm, not whenever someone remembers. A small fixed schedule turns the vague "did anyone check the system?" into a list of things that happen on known days:
| Cadence | What gets done |
|---|---|
| Daily | Health checks green, no stuck jobs |
| Weekly | Error dashboard scan, outdated-dependency check |
| Monthly | Review the error tracker, close stale issues, upgrade minor versions |
| Quarterly | Dependency upgrade cycle, docs freshness pass, debt review |
| Yearly | Full review with the customer: is this still the right tool? |
The point is that maintenance survives handoffs and busy weeks. When someone asks "when did we last check X?", there's an actual answer. The cadence also ties back to earlier chapters — a quarterly debt review is where the debt log gets re-prioritized, for instance.
Maintenance costs real time, and pretending it doesn't leads to skipped updates and hidden debt. A useful rule of thumb: a healthy, small long-term solution costs roughly 10–20% of a person's time to maintain. As it grows, so does that percentage.
Budget for it explicitly rather than hoping "it mostly takes care of itself." If the maintenance load on a solution grows past what it's worth — endless fixes, no time for improvements, no end in sight — that's not a failure of discipline. It's a signal. Maybe it was forked too many times (Tutorial 6). Maybe it's time for the next section: retiring it.
Not every solution should live forever. Customers change processes, tools get replaced, and a solution can outlive its usefulness. Retiring one well is the final form of maintenance — and doing it badly (turning it off one day, losing the data and the workflows) erases the value it created over its whole lifetime.
A good sunset is planned, communicated, and staged:
Announce: give users a clear end date, months ahead Communicate: explain what replaces it and where their data goes Export: make sure every user can get their data out Migrate: help users move to the replacement before the cutoff Remove: turn it off, keep backups until you're sure nothing was missed
Sunset timeline — legacy invoice importer 2026-09-01 Announce end date (2027-03-31), publish export guide 2027-01-01 Reminder to users not yet migrated 2027-03-15 Freeze changes; only critical fixes 2027-03-31 Turn off; keep backups 12 months
A well-retired solution is remembered as a smooth transition, not a surprise outage. The same care that kept it working for years is what makes its ending graceful.
Save your progress and earn XP for completing tutorials.
4 questions · Pass with 70%+
1What is the most common reason a customer solution gets abandoned after launch?
2Why should technical debt be tracked in a visible list?
3What does a "freshness check" on documentation involve?
4What's the recommended maintenance budget for a healthy, small long-term solution?
Technology
Forward Deployed Engineer
Lesson group
Production & Reliability
Progress
100% complete