Proving an app is dead

Over the last few weeks I took on a question that sounds simple: which of our apps can we turn off?

Every platform collects apps that outlive their purpose. Someone builds a thing, the need changes, the team moves on, and the thing keeps running. It still gets patched, planned, and watched. So I went through every app in our infrastructure code and asked each one the same question: is anyone using you?

The answer was harder to get than I expected. A live app leaves evidence. A dead app leaves silence, and silence has a lot of explanations.

Missing isn’t zero

The first trap was treating “no data” as “no use.”

If the load balancer has a target for an app, and that target reports zero requests for 90 days, that’s evidence. The meter exists, and it reads zero. But if an app doesn’t show up in the monitoring tool at all, that tells you nothing. It may never have been set up to report.

Some folders in the repo can’t report anything, because they don’t run anything. They hold a DNS record, a sign-in group, or the settings for a code repository. Calling those “dead” because they’re quiet is just noise. They get judged by their purpose and their git history, not by traffic.

And every kind of app needs its own meter. Two apps that looked dead were alive. One served its pages from a hosting service that reports traffic under a different tag than everything else. The other had moved off our platform entirely. So now, for every app, I write down two things: did each source see the app at all, and what did it report?

Logs aren’t traffic

The second trap was log volume. One quiet app wrote hundreds of thousands of log lines a month, which looked busy. Then I read them. The database writes its own logs under the app’s tag, and almost all of them were the database’s monitoring agent logging in to check on itself. When I counted only real web requests, a month of “activity” came down to a few dozen requests on a single day.

Health checks fooled me the same way. The load balancer pings each app every few seconds, and those pings don’t count as requests. So an app can write a log line every three seconds and still show zero traffic. That isn’t a contradiction. It’s a load balancer asking “are you there?” and the app saying yes.

One of the apps I turned off had not reached its database in months. Nothing alerted, because its health check only asked if it was running. It never asked if it could do its job.

Quiet has other reasons

Even real zero traffic doesn’t mean unwanted.

That same quiet app with all the database logs wasn’t abandoned. It wasn’t launched yet. A comment in its config reserved its web address for launch day, and its AI feature still pointed at a fake provider used for testing. No metric could see that. Only the code said it.

Other quiet things had good reasons too, such as test environments that only run during work hours, or a conference app that sits at zero most of the year and wakes up for one event. Before I call something idle, I read its own config first.

Turn it off before you tear it down

Two apps passed every check: no requests, no real logs, no callers. Even then, I didn’t delete them. I scaled them to zero and waited two weeks. Scaling down is easy to undo. Deleting isn’t. Nobody called, and nothing broke.

The hardest part wasn’t technical. It was finding someone who could say yes. The first person I asked hadn’t worked on these apps and pointed me to someone who had. That person approved it in one line and added the history I couldn’t find anywhere else: what the apps had been for, and what replaced them.

Archived isn’t safe

The teardown had one trap I almost missed. Our Terraform archives a repository on destroy instead of deleting it, so I wrote “archived, not deleted” in my notes and moved on. I hadn’t checked.

The same code also manages a branch in that repository, and it removes the branch before it archives the repo. One repo’s branch held 26 commits that existed nowhere else. An AI reviewer that I run on every change, before I open it, caught that before anything was applied. That miss was mine. We kept the branch, archived the repo, and now I compare branches before any teardown.

The code also only knows what the code made. These apps held logins in other systems that our infrastructure code never managed. Retiring those took a short list and a message to each system’s owner, and a couple of them are still in progress.

Start from the source

One more small lesson. My first list of empty Terraform folders came from a copy on my laptop that was hundreds of commits behind. It listed things that were already gone and missed newer ones. I rebuilt the list from the main branch, proved each folder was empty with a plan, and only then deleted them, in a separate change.

Knowing when to stop

After the first two apps, I looked at test environments that sat at zero while production ran fine. Most had a reason. Some were off on purpose, such as an example app other teams copy. Some were in use in ways the metrics didn’t show. Only a few were real candidates, and each one needed time from its owners. I closed that task without tearing anything down and wrote the candidates on it for later.

Money wasn’t the reason for any of this. The apps I removed cost very little to run. The payoff was a broken service gone, its failure logs gone, and less code for every future change to plan against. When the payoff is that small, other people’s time is the bigger cost.

The checklist

What I’d hand to anyone doing the same work:

A dead app doesn’t announce itself. It just goes quiet, and so do a lot of live ones. The work is telling them apart.