At 2:17 PM, we thought the SMS queue migration had been finished for several days.
The production cutover had passed a clean multi-day soak. The new queue was live. Nothing in the service behavior gave us a reason to keep the legacy path around. That is normally the moment a migration stops getting attention. The new system works, the dashboard is quiet, and everyone moves on.
The queue was only one part of what we changed. The final production state touched legacy services, a CI runner, working folders, an API repository, the main application repository, a shared runtime registry, and a standalone repository. The deployment had succeeded. The map of the deployment had not caught up.
For those several days we would have told anyone who asked that the migration was done, and we believed it. Production traffic supported that belief. A clean soak could not tell us that three repositories and a shared registry still disagreed with each other and with the runtime. We only found out because we went looking for a reason other than doubt. We were closing out the ticket properly.
The cutover was not the finish line
We did not shut down the old path the first time the new queue worked. We waited for the multi-day production soak, verified the cutover was clean, and only then started removing things. A queue migration can look healthy during a short test and still fail when a retry path, a scheduled job, or a quieter production condition finally appears.
Once the soak passed, we decommissioned the legacy services, the CI runner, and the folders that existed only to support the old path. Each removal closed a different gap. A runner left alive preserves a way to build or run stale code. Folders left around make the retired deployment look maintained. Services left registered tell an operator they are still available to start.
The tempting move was to call the migration done after the soak and file the cleanup for later. We skipped that, because stale infrastructure starts teaching people immediately. A service name in a runtime registry reads as an operational instruction. An idle CI runner is a future diagnostic branch, one more thing someone has to account for under pressure. We treated decommissioning as part of the production change.
Three repositories, three competing stories
The documentation audit had to cross three locations because the system was described in three locations. The API repository carried the code and service-facing integration around the queue. The main application repository carried the application-level operational picture. The shared runtime registry answered the most important question: what actually exists and can run.
The final WinSW service state was the anchor. We started there and compared each repository and the registry against it. We did not rewrite prose until it sounded current. For every record, we asked whether it described the same runtime that had survived production.
Starting from runtime state changes the question. If you start from documents, you ask whether each page seems plausible. If you start from the runtime, you ask something stricter: can this line make someone look for, start, deploy, or depend on something that no longer exists?
That is how stale service names surfaced. They were not necessarily breaking production at that moment. They were believable, which made them more dangerous. An engineer following one later could lose time on an abandoned path. An automation reading an old reference could preserve the wrong assumption. The production service can be correct while the organization still carries an expired model of it.
Two orphaned services, two different problems
The audit also turned up two services that did not belong in the final picture.
One was a dead proof of concept. It was not doing live work and did not belong to the queue. That still did not justify leaving it documented as runtime. A dead proof of concept makes the system look more complicated than it is, and it turns an old experiment into a false maintenance obligation.
The other was future scaffolding that had never been built. This was a different mistake. An idea had acquired the appearance of a deployed component because it entered the infrastructure record too early.
The cleanup was the same for both. Neither could be presented as live runtime. The audit checked the lifecycle status of every entry against reality.
When we find a service entry during a migration audit, we now sort it into one of four categories: live, retired, experimental, or future. A legacy service replaced by the cutover is retired, and its operational references need removal or correction. A proof of concept is experimental and cannot sit in live runtime documentation without that label. Future scaffolding stays future until a real deployment exists. A runtime registry that mixes those categories can no longer say what exists and can run. It reads as a notebook of possibilities.
Archive the history, remove the ambiguity
The standalone repository was the clearest artifact of the old architecture. Once the new queue had survived its soak and the legacy services, runner, and folders were gone, that repository no longer owned an active deployment path. We archived it and took it out of the normal working set.
Archiving says the code is retained as history and is no longer a current operational answer. That matters when someone searches across repositories during an incident, or when automation enumerates projects and finds old code with a familiar service name.
Keeping the repository active would have preserved another competing source of truth. Deleting it outright would have thrown away useful history. Archiving handled both concerns.
The closing pass we want on every migration
The migration was complete when the old runtime was removed, the CI runner and folders tied to it were gone, the standalone repository was archived, and the API repository, main application repository, and shared runtime registry all described the same final WinSW state. Processing production traffic on the new SMS queue was only part of that.
Our closing pass is now concrete. Let the new path survive a real production soak. Decommission every retired service and the tooling that exists only for it. Use final runtime state as the audit baseline, then compare every repository and registry against it. Classify every stray entry as retired, experimental, future, or live. Archive code that remains useful as history but no longer owns an active deployment path.
A clean deployment proves the new thing works. This audit showed the old thing was gone: seven places checked against one final WinSW state, two orphaned services relabeled out of live runtime, and one standalone repository archived so it could no longer compete with the truth.
A migration is finished when the old runtime, its tooling, and every record describing the system agree with the final production state.