- Who this is for — An engineer with production access and no dedicated FinOps function.
- What it assumes — A long-lived system nobody has audited in years.
- How long it takes — Days for the first two phases. Longer only if the third is justified.
- How to use it — In order. The order is the method, not a formatting choice.
- Companion volume — Each step carries an example prompt. Worked sessions expands them into full conversations.
Contents
- Access you need before you start
- Establish the baseline
- Cut, easiest first
- Structural change, only if justified
- Confirm it happened
- Traps
- What finished looks like
Access you need before you start
Most of this work is reading. Grant the reading permissions generously and the writing permissions late, because the analysis and the demolition are different jobs and only one of them can cost you an outage.
The billing and cost APIs
Not a screenshot of the console. You want the programmatic cost data, grouped and filtered, so you can slice by service, by usage type, by region and by day without asking anyone.
Read-only inventory across every service on the bill
Describe and list calls for compute, storage, databases, queues, load balancers. This is where most of the analysis actually happens.
Metrics and logs
Utilization history is the evidence for every claim that something is oversized. Request logs are the evidence for who still calls a service you want to delete.
The source code
You need it to prove that a resource is safe to remove, not to work out what is expensive. Code tells you what is complicated. That is a different question.
Somebody who remembers
The single most valuable input, and the one you cannot provision. Whoever knows which service was deprecated, which table is kept only for schema compatibility, and which fleet is deliberately frozen.
Caution. Do not do this work with an administrator credential just because one is lying around. A broad, long-lived key issued to a person or a tool carries the authority to delete production, and every action taken with it is indistinguishable from every other. Make a read-only identity for the analysis and keep the destructive steps deliberate and separate.
Establish the baseline
Phase I. Everything before this point is opinion.
Start from the bill, not the architecture
The instinct is to reason from the system diagram: find the component that looks heaviest and start there. This reliably picks the wrong target. The part of a system that occupies the most engineering attention is rarely the part that costs the most money, and a bill will often contain a line for something that appears in no repository at all — a managed service someone switched on years ago and forgot.
Pull the last full month, grouped by service. Read it before you form a theory.
Example prompt — attach the cost export
This is last month’s bill, grouped by service. Reconcile it against the architecture survey. What is on this bill that appears nowhere in the code, and for the largest lines, is the cost coming from reserved capacity, from request volume, or from stored data?
Then tell me which of your earlier conclusions this contradicts.
Read the bill for things that are not infrastructure
Before optimising anything, look for charges that no amount of engineering will fix: late payment fees, support plans nobody uses, reserved commitments covering capacity you retired, address and endpoint charges for things that are idle. These are the cheapest wins available and they require no risk assessment whatsoever.
Example prompt
Go through every line of this bill and separate out the charges that no code change will fix: fees, penalties, unused support plans, commitments covering retired capacity, idle addresses and endpoints.
For each, say who would have to act to stop it.
Group by region as well as by service
Analysis defaults to the region you work in, because that is where the system lives. Old estates rarely stay in one region. Somewhere there is a deployment that was decommissioned years ago, whose compute was stopped and whose storage was never released, quietly billing every month since. Nobody looks, because nobody works there.
Example prompt
Regroup the same period by region as well as by service. For any region I am not actively deploying to, inventory what is still there and when it last did anything.
Cut, easiest first
Phase II. Ordered by blast radius, not by size of prize.
Sweep for the dead
Stopped instances holding attached disks. Volumes with no attachment. Snapshots of machines that no longer exist. Queues and streams with no traffic. Load balancers with no targets. Registered container instances that stopped reporting long ago.
This is the best ratio of money to risk in the entire exercise, and it is almost pure arithmetic. Verify each one is genuinely dead — check the code, check the metrics, ask the person who remembers — then remove it.
Example prompt
Inventory across every region with a charge: stopped compute and its attached disks, volumes with no attachment, orphaned snapshots, queues and streams with no traffic in thirty days, load balancers with no healthy targets. Show the commands and their raw output.
Then for each candidate: what is your evidence it is dead, and what evidence would change your mind? Check the code for references before answering, and rank by confidence.
Change billing modes and hardware generations
Reserved-versus-consumption billing on databases, current-generation instance families, newer storage tiers, cheaper processor architectures. These are large, reversible, and change nothing about how the system is built. A machine from a decade-old family is usually both slower and dearer than its replacement, which makes the decision easy.
Example prompt
For every
‹resource type›on reserved capacity, pull consumed versus provisioned for the last sixty days.Then derive the break-even: at what utilization does pay-per-request stop being cheaper than the capacity it replaces? Show the arithmetic, and do reads and writes separately since they price differently. Split the resources into clearly below, clearly above, and too close to call.
Caution. Switching a database from reserved capacity to pay-per-request is not a saving, it is a trade. You are exchanging a fixed cost for a variable one, and you are removing a ceiling. That ceiling was also a signal: under reserved capacity a sudden increase in load shows up immediately as throttling. Under consumption billing it shows up quietly, next month, on the invoice. Set an alarm on the new variable, not just on the old bill.
Resize against measured load, and check the other side
Current utilization is the right input for sizing. It is not a sufficient one. It tells you what is happening now, at this moment, under whatever cache warmth and traffic mix happens to exist today. It does not tell you what breaks at peak, and it does not tell you how much headroom was absorbing variance you never had to think about.
Pair every resize with a latency objective you check afterwards, and hold the old capacity until you have checked it.
Example prompt
Here is the utilization history for
‹fleet›. Propose a target size — but first, tell me what this measurement cannot see. How much of the current memory is doing cache work that will move to the database if I shrink it, and what latency objective should I record now so I can tell afterwards whether this was a mistake?Days later, after the change: the resize is live. Compare p99 and error rate against what we recorded. Then check the whole bill, not just the compute line: did cost or request volume rise anywhere else in the same window?
Caution. Shrinking a fleet moves work rather than removing it. Smaller machines hold smaller caches, so the databases behind them see more reads. If those databases are billed per request, part of your compute saving reappears on the storage line. Two savings claimed in two places can be the same money spent once, and the only way to catch it is to look at the whole bill after the change, not just the line you set out to cut.
Structural change, only if justified
Phase III. Replacing a service is a different kind of project. Price it as one.
Justify the migration on obsolescence, not on the monthly line
Migrating off a managed service takes weeks or months, and the everyday work of the team continues around it. Measured purely in money saved per day of effort, it will almost always be the worst item on your list.
That does not make it wrong. Dead-end products with no roadmap, no upgrade path and no way to run the same thing elsewhere are worth leaving on their own terms, and the migration usually buys capability you could not otherwise have. Make that the argument. If you sell it internally as a cost measure, it will be judged as one, and it will lose.
Example prompt
I am considering replacing
‹service›with‹replacement›. Price it as a project rather than a line item: running cost at our real data volume, engineering time, and risk.Then argue against it. If the saving alone had to justify the effort, would this be approved? If not, what is the non-financial case — or is there one?
Build the switch first, then test parity, then delete
Put a configuration flag in front of the dependency before you write any migration code, so each consumer can move independently and move back. Then run both systems against real traffic and compare their answers, query by query, record by record, paging results to completion rather than sampling the first page.
Comparison is not the verification step at the end of the migration. It is the migration. It is where you find the differences that no test suite encodes: a field that was quietly enriched by the old pipeline, an encoding that survives one system and not the other, a sort that was case-insensitive by accident. Budget for it as the main activity.
Example prompt
Run
‹query›against both the old and new system. Page both to completion rather than comparing first pages, and show every record present in one and absent from the other.Then classify each difference: a bug in the new implementation, a bug in the old one we have been living with, or a behaviour the old system provided that we never specified. List that third group separately — those are the requirements nobody wrote down.
Caution. Dual-write always runs longer than planned. Design it so leaving it on for months is cheap and boring, rather than something that has to be rushed off.
Find the remaining callers in traffic, not in the repository
Before you delete the old service, search the code for references — and then do not trust the result. A long-lived estate contains deployed things whose source is stale, checked out nowhere, or built from a branch nobody remembers. Grep will return a large number of hits, you will clean them all up, and something you never saw will still be calling the endpoint.
Access logs and request metrics are the authority on who is still calling. Check them for a full traffic cycle, including whatever weekly job runs at an hour you are asleep.
Example prompt
I want to delete
‹service›. Do not rely on the code search, I have done that and I do not trust it. Use access logs and request metrics from the last full week to tell me what is still calling it, when each caller was last seen, and whether anything only appears at a particular hour.Also confirm whether this service can be restored after deletion, and whether a recreated one comes back at the same address.
Caution. Confirm the deletion is reversible before you rely on being able to reverse it. Many managed services cannot restore a deleted instance, and a recreated one comes back with a new address, which means callers stay broken even after you undo your mistake.
Confirm it happened
Phase IV. This is the phase that gets skipped, and it is the one that makes the rest true.
Close every decommission explicitly
A migration saves nothing until the old thing is destroyed. Running the cheap replacement alongside the expensive original is a cost increase, and it is the correct thing to be doing while you verify — the overlap is the verification window. The failure is not opening it, it is forgetting to close it.
Keep an explicit list of what is still running only as a fallback, with the condition that retires each one. Then re-check after every removal: deleting a machine often leaves its disks behind, still billing, and the cleanup looks complete because the thing you were looking at is gone.
Example prompt
List everything currently running only as a fallback, with the condition that would retire each one.
Then re-run the inventory filtered to unattached storage and stopped instances. I terminated things recently and I want to know what they left behind.
Reconcile against a real invoice, line by line
Every number produced during this work is an estimate. Estimates are built on assumptions about load that were true on the afternoon you measured, and they are wrong in both directions: some changes deliver more than predicted, some deliver almost nothing because a different variable moved underneath them.
Compare the new invoice to the baseline, per service, and account for every line that did not move as expected. An estimate that is never checked is a claim, not a result. Put the reconciliation in the calendar before you start cutting, because afterwards there is always something more interesting to do.
Example prompt
Here is the baseline and here is the current invoice. Compare per service, and mark each saving I claimed as confirmed, partial or failed. List any service that got more expensive, whether or not I touched it.
Then take the one that failed hardest: break it down by usage type, find the date the wrong component started rising, and correlate that date against everything else that changed. Tell me which parts you have evidence for and which are inference.
Leave guardrails behind
This kind of work is usually reactive: something looked alarming and somebody went and dealt with it. Without a standing check the same conditions return, because the pressures that produced them have not changed.
A budget alarm, cost allocation tags that survive contact with reality, an alert on the variable you just uncapped, and a short recurring review are enough. The goal is that the next surprise arrives as a notification rather than as an invoice.
Example prompt
Write up everything outstanding as a checklist I can commit to the repository: deferred items, resources kept only as fallbacks with their retirement conditions, and anything we decided not to do and why.
Then propose the smallest set of alarms that would have caught each problem we found here, including the ones we found by accident.
Traps
These are not sequential. They are the things that make a cost exercise report a saving it did not achieve.
- Stopped is not deleted. A stopped machine bills nothing for compute and everything for its attached storage, silently, forever. It survives for years precisely because it looks harmless.
- Deleted is not deleted either. Terminating a machine frequently leaves its data volumes behind, because they were configured not to be removed with it. Re-run the inventory filtered to unattached storage after every removal.
- Resource count is not cost. Auditing hundreds of objects and deleting the idle ones feels like the headline result and is usually worth very little. The money is in throughput and storage volume, not in the number of things. Optimise the invoice, not the inventory.
- A ceiling is also a signal. Removing a capacity limit removes the mechanism that would have told you when load changed. What used to surface as throttling within minutes now surfaces as an invoice next month.
- Two savings can be one saving. Changes interact across service boundaries. Work removed from one line reappears on another, and both teams book the win.
- Utilization is measured, not intrinsic. What a system consumes today is a function of what is in front of it today. Change the cache, the fleet size or the traffic mix and the measurement you sized against no longer describes anything.
- Grep is not a caller list. Source control describes what you can build. It does not describe what is running.
- The migration is the comparison. Writing the new integration is the small half. Finding every behaviour the old system provided by accident is the large one.
- Nothing is saved until it is billed. Until an invoice confirms it, the number is a forecast with your name on it.
What finished looks like
- Every line on the baseline invoice is either explained, reduced, or explicitly accepted.
- Each claimed reduction has been matched against a later invoice, including the ones that did not land.
- Nothing is running solely as a fallback without a written condition for retiring it.
- Every service you resized has a latency check that ran after the change, not before.
- Any variable cost you introduced has an alarm on the variable itself.
- The deferred cleanup list lives somewhere durable, not in a chat window.
Written from a real engagement on a long-lived production estate. The order of the phases reflects what went wrong, and in which order.