Somewhere on the internet, an application built for a temporary event is still answering requests years after everyone involved has moved on. One surprisingly good illustration is a Bowdoin Reunion ride board from 2013, where the surrounding event belongs to another era of the web while the discussion surface has continued to host much newer activity. The interesting part is not that an old page survived. The interesting part is that software can outlive its purpose without outliving its capabilities.
Developers are trained to think about software in terms of what exists in the current product: the routes in the current repository, the services visible in the current architecture diagram, the APIs documented in the current OpenAPI specification.
The internet does not care about any of those boundaries.
If an endpoint still answers, it exists.
If a form can still submit, it exists.
If an old API version can still reach production data, it exists.
If an abandoned subdomain still resolves to infrastructure, it exists.
And if your engineering team has forgotten it exists, an external observer may understand that part of your system better than you do.
That is the uncomfortable engineering problem behind what we can call zombie functionality: features that are dead organizationally but alive technically.
“Nobody Uses It” Is Not a System State
Teams use surprisingly imprecise language when software gets old.
“We don't use that anymore.”
“That was for the old site.”
“I think marketing stopped using it.”
“That API was replaced two years ago.”
“That admin panel belongs to the previous platform.”
None of these statements tells us what the computer will do when someone sends it a request.
There is a huge difference between deprecated, hidden, unlinked, unsupported, read-only, access-restricted, and actually removed. Inside a company, those states can feel roughly equivalent because employees no longer encounter the feature in normal workflows. From outside the company, they are completely different.
Removing a navigation link does not remove a route.
Deleting the frontend button does not disable the backend operation.
Launching /api/v3/ does not automatically kill /api/v1/.
Moving employees to a new admin portal does not make the previous portal unreachable.
This distinction sounds obvious when written down. In real systems, it is repeatedly missed because product retirement is usually treated as an organizational decision rather than a technical lifecycle.
A feature gets a launch process.
It rarely gets a death process.
Software Has an Afterlife
Modern software development optimizes aggressively for creation.
We have templates for starting services, infrastructure-as-code for provisioning resources, CI/CD pipelines for shipping builds, feature flags for activating functionality, observability for watching it, and dashboards for measuring adoption.
There is much less infrastructure for proving that something is gone.
Imagine an internal tool launched in 2020.
The first version lives at admin.example.com.
In 2022, the company rebuilds it at console.example.com.
Everyone migrates. Documentation is updated. The old link disappears from Notion. A year later, half the engineers who built the first version have left.
What happened to admin.example.com?
Maybe the DNS record still exists.
Maybe a load balancer still knows where to send the request.
Maybe the old application still authenticates against the same identity provider.
Maybe the application uses an old authorization model.
Maybe it shares a database with the new console.
Or maybe it returns a 404.
The important point is that you cannot infer its technical state from its organizational state.
This is why forgotten functionality is such an interesting class of engineering debt. Ordinary technical debt is visible because engineers encounter it while changing the system. Zombie functionality can become less visible precisely because nobody changes it.
Its lack of activity protects it from attention.
Attack Surface Is an Inventory Problem Before It Is a Vulnerability Problem
Security discussions often begin with vulnerabilities: outdated dependencies, injection, broken authorization, leaked credentials.
But before you can secure an asset, you have to know that you have it.
That sounds trivial until an organization has accumulated acquisitions, cloud accounts, test environments, regional deployments, SaaS integrations, old domains, contractor-built applications, temporary campaign sites, preview environments, versioned APIs, migration infrastructure, and years of DNS history.
A useful way to understand the problem comes from Google Cloud’s explanation of external attack surface management, which focuses on continuously discovering internet-facing assets rather than assuming the internal inventory is complete.
That word — discovering — matters.
Traditional asset management asks:
“What systems do we own?”
External discovery asks:
“What systems can somebody on the internet find that appear to belong to us?”
Those datasets should be identical.
In a mature organization, they often are not.
And the difference between them is where some of the most interesting risks live.
The Frontend Can Die While the Capability Survives
One reason zombie functionality is easy to create is that users experience products through interfaces, while systems expose capabilities through layers.
Suppose a company has a profile page with a “Delete account” button.
Later, the UI is redesigned and the button moves into a new settings application. The previous settings page is removed from the router.
From the product team's perspective, the old feature is gone.
But perhaps the original request was:
POST /legacy/account/delete?id=1234
If that handler still exists, the capability has not disappeared. Only one path humans used to discover it has disappeared.
This distinction becomes especially important with APIs because APIs are designed to be called directly. They do not need a visible button.
A deprecated mobile application may therefore leave behind an API generation long after the application itself has vanished from app stores.
A retired integration may leave behind OAuth scopes.
A discontinued partner program may leave callback handlers.
A removed upload screen may leave the upload endpoint.
A beta feature may leave database fields, jobs, queues, permissions, and routes long after the feature flag is turned off.
What users see as one feature may correspond to dozens of independently surviving technical artifacts.
Deleting the visible part is easy.
Proving that the capability has disappeared is harder.
The Real Problem Is Ownership Decay
Code does not literally become more dangerous because it is old.
Ownership decays.
The engineer who understood why an endpoint bypasses the normal service layer leaves.
The team that operated a microsite is dissolved.
The vendor that built the original portal is replaced.
A cloud subscription moves between departments.
A database gets inherited by another application.
The source repository is archived.
Monitoring is pointed at the new system.
The old system continues answering requests.
Eventually you reach a strange condition: the company still owns the software, but nobody meaningfully owns its behavior.
This is more serious than missing documentation.
Ownership determines who receives alerts, approves dependency upgrades, understands data flows, responds to incidents, evaluates changes in authentication requirements, and makes the decision to finally shut something down.
Without an owner, “temporary” infrastructure has a remarkable tendency to become permanent.
The Internet Has Better Memory Than Your Org Chart
There is another asymmetry developers should understand.
Internal knowledge disappears when people leave and systems change.
External evidence often remains.
Certificate Transparency logs can reveal historical hostnames. Search engines remember paths. Old JavaScript bundles contain API routes. Public Git repositories preserve configuration history. Documentation forks survive after official docs are rewritten. Browser extensions and mobile binaries can preserve references to servers long after those products stop being maintained.
This means removing something from your current architecture diagram does not necessarily make it undiscoverable.
Attack-surface discovery tools work from exactly this assumption. Cloudflare’s discussion of attack surface intelligence describes outside-in discovery of assets such as forgotten subdomains and unmanaged infrastructure — things an internal security workflow can miss because nobody remembered to put them into the inventory in the first place.
There is an important engineering lesson here:
Documentation describes what your organization believes exists. The network reveals what actually exists.
Those are not always the same system.
Deprecation Should Be a State Machine
One reason software retirement goes badly is that “deprecated” is treated as a label.
It should be a sequence of technical states.
A feature should move from something like ACTIVE to DEPRECATED, then perhaps READ_ONLY, then DISCONNECTED, and finally REMOVED.
Each transition should have observable consequences.
When an API becomes deprecated, new consumers should no longer be allowed to register against it.
When it becomes read-only, mutating methods should stop succeeding.
When it is disconnected, application traffic should no longer reach sensitive downstream systems.
When it is removed, infrastructure should disappear rather than merely stop appearing in documentation.
Most importantly, the transition should have an owner and an expiry date.
“Deprecated indefinitely” is another way of saying “production forever.”
Build the Exit Before You Launch
Developers usually think about decommissioning when they are already trying to decommission something.
That is too late.
The easiest system to retire is one designed with retirement in mind.
If you are building a temporary microsite, decide at launch what happens to it after the campaign.
If you are introducing /v2/, decide when /v1/ loses write access.
If you create a preview environment, decide what deletes it.
If you launch a feature flag, decide whether disabling it removes only UI exposure or also backend capability.
If you provision a new public hostname, give it an owner at creation time.
A useful production lifecycle check can be surprisingly compact:
- What publicly reachable asset is being created?
- Who owns it after the original project ends?
- What sensitive systems or data can it reach?
- How will we know whether legitimate traffic still depends on it?
- What exact event moves it into retirement?
- What proves that retirement actually completed?
- What prevents the same asset from silently reappearing during a rollback or old deployment?
The point is not to create more paperwork.
The point is to make deletion as real an engineering operation as deployment.
Start Measuring Negative Space
Most engineering metrics measure things that are happening.
Requests per second.
Error rate.
Latency.
CPU.
Active users.
Deploy frequency.
Conversion.
Zombie systems often require the opposite question:
What is still reachable even though almost nothing legitimate should be happening there?
That makes low traffic interesting.
Imagine an endpoint that normally receives five requests per month.
Traditional monitoring may consider it irrelevant.
But if the product containing that endpoint was retired two years ago, five requests are not “low usage.” They are unexplained usage.
The baseline should be zero.
The same applies to old domains, deprecated APIs, retired OAuth clients, stale service accounts, abandoned queues, and forgotten cloud functions.
Engineering organizations are good at detecting unexpected increases.
They should become equally good at detecting unexpected survival.
Do Not Confuse a 404 With Decommissioning
Even apparently dead systems can remain alive underneath.
A reverse proxy can return a 404 while the original application continues running behind it.
A firewall rule can block public access while an obsolete service retains broad internal network access.
A DNS record can be removed while cloud resources continue accumulating cost and credentials remain valid.
A frontend deployment can disappear while storage buckets and serverless functions survive.
Real decommissioning therefore requires working backward through dependencies.
What identity can access the system?
What identities can the system access?
What databases does it know about?
What secrets does it hold?
What queues does it consume?
What webhooks point to it?
What scheduled jobs invoke it?
What DNS records reference it?
What certificates were issued for it?
What logs would prove that something still depends on it?
This is essentially garbage collection for infrastructure, except reference counting is much harder because many references live outside the system you control.
Temporary Software Is Often the Least Temporary Software
Some of the strangest long-lived systems begin as short-term projects.
Conference registration sites.
University event boards.
Marketing campaigns.
Election dashboards.
COVID-era portals.
Hackathon applications.
Product betas.
Migration bridges.
Emergency internal tools.
Temporary systems are often built under deadline pressure, with weaker ownership structures and an assumption that somebody will remove them later.
Then they work.
And because they work, nobody wants to touch them.
The project ends, priorities change, the original team moves on, and “remove old site” loses every roadmap battle against new revenue-generating work.
Ironically, the feature nobody expected to survive ten years may receive less lifecycle engineering than the platform explicitly designed to survive ten years.
That is how temporary becomes permanent.
The Best Security Improvement May Be Subtraction
Software engineering has a strong bias toward adding controls.
Add authentication.
Add a WAF.
Add monitoring.
Add another scanner.
Add another policy.
Sometimes the cleanest control is to remove the capability entirely.
An endpoint that does not exist needs no patch.
A deleted cloud account needs no monitoring.
A removed API version needs no authorization policy.
A retired admin interface cannot accidentally retain yesterday's permissions.
A destroyed credential cannot leak next year.
This is one of the rare areas where maintainability, cost, architecture, and security all point in the same direction.
Smaller systems are easier to understand because there are fewer truths to keep synchronized.
The challenge is that subtraction requires certainty. Teams hesitate to delete old systems because they do not know what depends on them.
Which brings us back to inventory.
You cannot confidently remove what you cannot confidently map.
Every Deployment Creates a Future Legacy System
Developers rarely think of the service they are launching today as legacy software.
But unless it fails immediately, that is exactly what it is becoming.
Frameworks will change.
Teams will change.
Authentication systems will change.
Domains will be reorganized.
APIs will be replaced.
Infrastructure providers will come and go.
The question is not whether today's architecture becomes old.
The question is whether the organization retains enough control over its lifecycle to know when old software is still doing real work.
The most important mental shift is simple:
Production is not the set of systems your team currently works on. Production is the set of systems that can still do something.
Once you use that definition, neglected endpoints stop being digital archaeology.
They become current architecture.
And perhaps the most valuable question in your next architecture review is not “What are we building?”
It is:
What is still running that nobody in this room remembers building?
Top comments (0)