Summary
- Client: An enterprise financial services company spending more than $100,000 a month on Azure (anonymized).
- Problem: Azure SQL accounted for a large share of the bill, and the team suspected some databases were left over from earlier migrations, but couldn’t tell which were safe to remove.
- Finding: Seven databases, costing $12,600 a month, were temporary copies from a completed migration. The engineer who led it had left, and the cleanup was never assigned to anyone.
- Outcome: All seven were exported, scaled down in stages and then deleted over ten days, with no impact on production. Savings: $12,600 a month, or $151,200 a year.
The situation
The client’s VP of Infrastructure was under pressure to reduce an Azure bill of more than $100,000 a month, and databases were a large part of it. The team suspected that some Azure SQL databases were left over from earlier migrations, but with financial data involved, nobody was willing to remove anything without proof that it was unused.
The request was specific: identify any orphaned databases, and do it without putting production systems at risk.
The database estate was substantial:
- 23 Azure SQL databases across three regions
- 4 Azure SQL Hyperscale deployments for core banking
- 6 read replicas for reporting
- Total Azure SQL spend: $31,800 a month
Identifying the candidates
Billing patterns in Azure Cost Management highlighted seven databases, and utilization data from Azure Monitor confirmed they stood out from the rest:
Utilization of the seven flagged databases (Azure Monitor):
- Fewer than 5 connections a day for more than 90 days
- 1 Business Critical BC_Gen5_40 database ($6,830/month) with no activity
- 6 General Purpose databases ($5,770/month combined)
- No writes to any of the seven
Verifying they were unused
KQL queries against Azure Monitor logs gave a consistent picture across three lines of evidence.
Connection history
- No application connections for 95 days
- The only connections came from monitoring tools
- The last authenticated user was the migration project’s service account
- The last substantial data access was on October 12, 2023
Data changes
Last DML and DDL operations:
- INSERT: 95 days ago
- UPDATE: 95 days ago
- DELETE: 95 days ago
- TABLE CREATE: 95 days ago
- TABLE ALTER: 95 days ago
Cost
Databases flagged:
- 1x Business Critical (BC_Gen5_40) $6,830/month
- 2x General Purpose (GP_Gen5_8) $2,280/month
- 4x General Purpose (GP_Gen5_4) $3,490/month
- Total: $12,600/month
How the databases came to exist
The Azure Activity Log from 95 days earlier explained where the databases came from. The entry that created one of them (identifiers anonymized) carried a tag stating its purpose:
{
"eventTimestamp": "2023-10-12T14:23:17Z",
"resourceProviderName": {
"value": "Microsoft.Sql"
},
"eventName": {
"value": "Microsoft.Sql/servers/databases/write"
},
"status": {
"value": "Succeeded"
},
"caller": "migration-lead@example.com",
"resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/prod-data-rg/providers/Microsoft.Sql/servers/sql-prod-01/databases/prod-migration-temp-db1",
"properties": {
"resource": "prod-migration-temp-db1",
"tags": {
"Purpose": "Temporary database for Hyperscale migration. DELETE AFTER VERIFICATION."
}
}
}
The databases were temporary copies created during a migration to Azure SQL Hyperscale. The migration had been completed successfully, but the cleanup step never happened.
Conversations with the people involved showed why. The engineer who led the migration had left the company three months earlier. Their manager assumed the DevOps team had removed the temporary resources. The DevOps team had been told the migration was still being verified, never received confirmation that cleanup could go ahead, and after a few weeks assumed the databases were still needed. The tag said exactly what should happen, but nobody owned the task of doing it.
Confirming it was safe to remove them
Before recommending deletion, we confirmed that the databases were migration artifacts and that nothing depended on them:
- Schema comparison: a 100% match with the production databases
- Data timestamps: all data was 95 days old
- Application dependencies: no active connections from any application
- DNS: no internal services resolved to these databases
Managing the risk
Because the databases held financial data, the removal was planned as a medium-risk change, with these controls:
- Backups first. Each database was exported as a BACPAC file to Azure Blob Storage before any change.
- Staged scale-down. The databases were scaled down to a minimal service tier in groups, which cut their cost and would surface any hidden dependency before anything was deleted.
- Monitoring between stages. The team waited 72 hours between groups, watching for errors or connection attempts.
- Documented approval. The CTO and VP of Engineering signed off on the plan.
Execution
- Day 1: Exported all seven databases to BACPAC files and scaled the BC_Gen5_40 database down.
- Day 4: No issues reported. Scaled down the two GP_Gen5_8 databases.
- Day 7: No issues reported. Scaled down the four GP_Gen5_4 databases.
- Day 10: Final confirmation, then deleted all seven databases. The BACPAC exports were kept for 90 days.
Results
- Monthly savings: $12,600
- Annual savings: $151,200
- Return: 900% (annual savings compared with the cost of the investigation)
- Time to resolution: 14 hours of investigation, plus 10 days of staged execution
Contributing factors
Several gaps combined to leave the databases running:
- A departure: the knowledge of what the databases were for left with the engineer who created them.
- No ownership of the cleanup: the cleanup step was never formally handed over to anyone.
- Reasonable caution: the remaining team didn’t feel safe removing resources they hadn’t created.
- No inventory of temporary resources: a tag was the only record of the databases’ purpose.
- No alerting on idle cost: nothing flagged expensive databases with no activity.
Prevention measures
Implemented with the client:
- Expiry dates for temporary resources, required by Azure Policy, so every temporary resource has a date by which it must be reviewed.
- Migration runbooks that include cleanup as an explicit step with a named owner.
- A handover process that transfers open responsibilities when someone leaves.
- Cost anomaly alerts in Azure Cost Management, plus a weekly Azure SQL utilization report that lists databases with few or no connections.
Takeaways
This case shows how a well-run organization can lose significant money through a single process gap. The migration itself was a success. What failed was the step after it, because nobody owned it.
The careful, staged approach mattered as much as the finding. Financial data called for caution, and exporting, scaling down in stages and waiting between steps meant the savings were recovered with no risk to business continuity.
Related case studies:
Find abandoned databases in your own subscriptions
These seven databases sat idle for 95 days: the engineer who led the migration had left, and even with a purpose tag reading “DELETE AFTER VERIFICATION”, nobody could say who owned them or whether they were still needed. Cloud Sleuth is being built as the ownership and context layer for cloud infrastructure, so every resource has a clear answer to who owns it, what it’s actually for and whether it should still exist. The Cloud Sleuth CLI will run on your machine or in your VPC, with nothing leaving your environment, and will cover Azure and AWS from launch, showing unowned resources, common orphans and basic allocation gaps in your own subscriptions and accounts. Both the local tool and Cloud Sleuth Cloud are pre-launch. Get early access.