Skip to content
case CS-002-AZ cost optimization

Seven Abandoned Azure SQL Databases Costing $12,600 a Month: A Migration Cleanup Case Study

An anonymized case study: seven Azure SQL databases left behind after a migration cost a financial services company $12,600 a month. How they were verified as unused and removed without risk to production.

Updated Cloud Sleuth

Engagement details

Resources reviewed
23
Costs analyzed
$31,800/month
Time spent
14 hours
Azure Cost ManagementAzure Activity LogAzure Monitor (KQL)Data footprint analysis

Outcome

Monthly savings
$12,600
Annual savings
$151,200
Resources cleaned
7
Risk level
MEDIUM
Client satisfaction
9/10

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:

  1. Backups first. Each database was exported as a BACPAC file to Azure Blob Storage before any change.
  2. 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.
  3. Monitoring between stages. The team waited 72 hours between groups, watching for errors or connection attempts.
  4. 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.

Cloud Sleuth CLI

Find out who owns your AWS and Azure resources

Get early access to the free Cloud Sleuth CLI. It maps your accounts, puts an owner on every resource, tagged or not, and shows what's safe to remove. It runs on your machine or in your VPC, and nothing leaves your environment.

Related case studies

case CS-003

23.2 TB of Inactive S3 Data: A Storage Ownership Case Study

An anonymized case study: 23.2 TB of S3 data had gone untouched for more than two years, and much of it had no clear owner. How access analysis and owner interviews moved it to Glacier Deep Archive without deleting a file.

$523 /mo saved 10 hours to solve