Azure SQL – Consider Limiting vCores and Capacity in Non-Production Projects

Azure SQL – Consider Limiting vCores and Capacity in Non-Production Projects

This FinOps policy requires that Azure SQL databases in non-production environments use reduced vCore counts and lower capacity tiers compared to production. Applying this constraint directly reduces cloud costs by preventing teams from running over-provisioned database resources in environments where peak performance is not required.

Why This Policy Matters

Non-production environments such as development, staging, and testing rarely require the same compute capacity as production. When Azure SQL databases in these environments are provisioned at production-level vCore counts or service tiers, the result is consistent, avoidable overspend.

This is a common source of wasted spend. Teams often copy production Terraform configurations into lower environments without adjusting capacity parameters, which means non-production workloads consume resources priced for high-throughput production use.

How It Helps Reduce Costs

Azure SQL in the vCore purchasing model charges directly based on the number of vCores provisioned. Reducing vCores in non-production environments scales cost down proportionally.

Moving from a General Purpose 8 vCore instance to a General Purpose 2 vCore instance reduces the per-hour compute cost by approximately 75%. Combined with lowering the storage tier or switching to the Serverless compute tier, the savings compound quickly.

This policy also encourages teams to use the Serverless compute tier for non-production Azure SQL databases. Serverless automatically pauses during periods of inactivity and resumes on demand, which eliminates charges for idle time entirely.

Potential Savings

Consider a non-production Azure SQL database running General Purpose, 8 vCores in East US.

  • Approximate monthly cost: $740/month

  • Reduced to General Purpose, 2 vCores: approximately $185/month

  • Savings: approximately $555/month per database

For organizations with 10 or more non-production databases, applying this policy across environments can reduce Azure SQL spend by $50,000 or more per year.

Switching to the Serverless compute tier for dev and test databases that are idle outside business hours can extend savings further, typically reducing costs by an additional 40-60% compared to provisioned Serverless or standard General Purpose.

Implementation Guide

Infrastructure-as-Code Example (Terraform)

The following example shows a common misconfiguration where a non-production Azure SQL database is provisioned with the same capacity as a production workload.

# Non-compliant: Non-production database using production-level vCores
resource "azurerm_mssql_server" "example" {
  name                         = "sql-server-dev"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "sqladmin"
  administrator_login_password = var.admin_password
}

resource "azurerm_mssql_database" "non_compliant" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_8"   # 8 vCores - over-provisioned for non-production
  max_size_gb  = 500
}
# Non-compliant: Non-production database using production-level vCores
resource "azurerm_mssql_server" "example" {
  name                         = "sql-server-dev"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "sqladmin"
  administrator_login_password = var.admin_password
}

resource "azurerm_mssql_database" "non_compliant" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_8"   # 8 vCores - over-provisioned for non-production
  max_size_gb  = 500
}
# Non-compliant: Non-production database using production-level vCores
resource "azurerm_mssql_server" "example" {
  name                         = "sql-server-dev"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "sqladmin"
  administrator_login_password = var.admin_password
}

resource "azurerm_mssql_database" "non_compliant" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_8"   # 8 vCores - over-provisioned for non-production
  max_size_gb  = 500
}

This configuration provisions a General Purpose Gen5 instance with 8 vCores. In a non-production environment, this level of compute is rarely needed and adds unnecessary cost.

# Compliant: Non-production database with reduced vCores (provisioned)
resource "azurerm_mssql_database" "compliant_provisioned" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_2"   # 2 vCores - right-sized for non-production
  max_size_gb  = 50
}

# Compliant: Non-production database using Serverless compute tier
resource "azurerm_mssql_database" "compliant_serverless" {
  name                        = "db-dev-serverless"
  server_id                   = azurerm_mssql_server.example.id
  collation                   = "SQL_Latin1_General_CP1_CI_AS"
  license_type                = "LicenseIncluded"
  sku_name                    = "GP_S_Gen5_2"   # Serverless, 2 vCores max
  max_size_gb                 = 50
  auto_pause_delay_in_minutes = 60              # Auto-pause after 60 minutes of inactivity
  min_capacity                = 0.5             # Minimum vCores when active
}
# Compliant: Non-production database with reduced vCores (provisioned)
resource "azurerm_mssql_database" "compliant_provisioned" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_2"   # 2 vCores - right-sized for non-production
  max_size_gb  = 50
}

# Compliant: Non-production database using Serverless compute tier
resource "azurerm_mssql_database" "compliant_serverless" {
  name                        = "db-dev-serverless"
  server_id                   = azurerm_mssql_server.example.id
  collation                   = "SQL_Latin1_General_CP1_CI_AS"
  license_type                = "LicenseIncluded"
  sku_name                    = "GP_S_Gen5_2"   # Serverless, 2 vCores max
  max_size_gb                 = 50
  auto_pause_delay_in_minutes = 60              # Auto-pause after 60 minutes of inactivity
  min_capacity                = 0.5             # Minimum vCores when active
}
# Compliant: Non-production database with reduced vCores (provisioned)
resource "azurerm_mssql_database" "compliant_provisioned" {
  name         = "db-dev"
  server_id    = azurerm_mssql_server.example.id
  collation    = "SQL_Latin1_General_CP1_CI_AS"
  license_type = "LicenseIncluded"
  sku_name     = "GP_Gen5_2"   # 2 vCores - right-sized for non-production
  max_size_gb  = 50
}

# Compliant: Non-production database using Serverless compute tier
resource "azurerm_mssql_database" "compliant_serverless" {
  name                        = "db-dev-serverless"
  server_id                   = azurerm_mssql_server.example.id
  collation                   = "SQL_Latin1_General_CP1_CI_AS"
  license_type                = "LicenseIncluded"
  sku_name                    = "GP_S_Gen5_2"   # Serverless, 2 vCores max
  max_size_gb                 = 50
  auto_pause_delay_in_minutes = 60              # Auto-pause after 60 minutes of inactivity
  min_capacity                = 0.5             # Minimum vCores when active
}

The Serverless option is recommended for databases that experience intermittent or unpredictable usage, which is typical in development and test environments.

Manual Step-by-Step Instructions

  1. Inventory all Azure SQL databases across non-production subscriptions or resource groups.

  2. Identify the vCore count and SKU for each database. Flag any database using more than 2-4 vCores in non-production.

  3. Determine usage patterns for each flagged database. Check CPU and connection metrics in Azure Monitor to confirm low utilization.

  4. Update the Terraform configuration to reduce sku_name to a lower vCore tier (e.g., GP_Gen5_2).

  5. Evaluate Serverless eligibility for databases that sit idle overnight or on weekends. Set auto_pause_delay_in_minutes to an appropriate value for your team’s workflow.

  6. Reduce max_size_gb to match actual storage needs. Non-production databases rarely require hundreds of gigabytes.

  7. Apply the changes via your standard Terraform plan and apply pipeline.

  8. Verify cost impact using Infracost before and after the change to confirm the expected reduction.

  9. Document the standard for non-production Azure SQL in your internal IaC policy library or module defaults.

Best Practices

  • Enforce vCore limits through Terraform modules. Create internal modules for Azure SQL that default to reduced vCore counts for non-production environments and require explicit override with justification for anything higher.

  • Tag resources by environment. Use consistent tagging (e.g., environment = “dev”) to make policy enforcement and cost filtering easier.

  • Use Serverless for idle non-production databases. Serverless auto-pause eliminates compute charges during inactivity, which is appropriate for most dev and test workloads.

  • Set lower max_size_gb values. Storage provisioning also contributes to cost. Non-production databases rarely need more than 32-100 GB.

  • Review vCore allocations on a monthly basis. Team requirements change, and databases that started at 2 vCores may have drifted upward without review.

  • Restrict who can modify vCore settings in non-production. Use Azure Policy or Terraform Sentinel to prevent provisioning above a defined vCore threshold in non-production environments.

  • Prefer the General Purpose tier over Business Critical in non-production. The Business Critical tier offers higher IOPS and built-in replicas, which are unnecessary for development workloads.

Tools and Scripts

Infracost detects Azure SQL vCore configuration and calculates the monthly cost of each database resource in your Terraform code. This policy is available in Infracost, including in the free trial.

When Infracost is integrated into your CI/CD pipeline, it automatically surfaces the cost impact of Azure SQL configurations at pull request time. Engineers can see the cost difference between an 8 vCore and a 2 vCore configuration before the change is applied.

Infracost also supports policy checks that flag Azure SQL resources in non-production environments exceeding defined vCore thresholds. This prevents violations from reaching production and gives FinOps teams a consistent enforcement mechanism without manual review.

Teams can use Infracost’s cost reporting features to track progress over time, identifying how much has been saved as non-production databases are right-sized and how many violations remain open across the estate.

Azure Policy can enforce SKU restrictions at the subscription or resource group level. Use the built-in policy definition Allowed values for Azure SQL Database SKU or define a custom policy to block deployments above a specified vCore count.

Azure Advisor surfaces cost recommendations for underutilized SQL databases. Review Advisor output monthly as a secondary input to your right-sizing workflow.

Examples of Impact

Example 1: Development environment consolidation

A software team maintained 12 Azure SQL databases across development and staging environments. Each was provisioned as GP_Gen5_8 (8 vCores), copied directly from the production Terraform module. After applying this policy and reducing all non-production databases to GP_Gen5_2, the team reduced Azure SQL spend in those environments from approximately $8,900/month to approximately $2,200/month, a saving of $6,700/month.

Example 2: Serverless adoption for overnight idle databases

A QA team ran four Azure SQL databases for automated testing. The databases were only active during business hours, approximately 10 hours per weekday. Migrating all four to the Serverless compute tier with a 60-minute auto-pause setting reduced compute charges by approximately 65% compared to provisioned 2 vCore instances. This added a further $480/month in savings on top of the initial vCore reduction.

Considerations and Caveats

  • Performance requirements vary. Some non-production workloads, such as load testing or data migration testing, may require higher vCores. These cases should be documented and provisioned on a time-limited basis rather than permanently.

  • Serverless has connection latency on resume. The first connection after an auto-pause event incurs a cold start delay of up to 60 seconds. This is acceptable for most dev and test scenarios but may disrupt automated test pipelines if not accounted for.

  • Serverless is not available on all SKUs. The Serverless compute tier is available for General Purpose vCore databases only. It is not available for Business Critical or Hyperscale.

  • Min vCore settings affect billing. When using Serverless, setting min_capacity to 0 enables full auto-pause. Setting it above 0 means the database is billed at the minimum vCore count even during inactivity.

  • Database feature parity. Some Azure SQL features behave differently across tiers. Verify that any features required for testing (such as geo-replication or zone redundancy) are available on the target SKU before downgrading.

  • This policy applies to vCore-based purchasing. If your organization uses the DTU-based purchasing model, apply equivalent constraints using DTU tiers (e.g., restricting non-production to S2 or S3 rather than P1 or P2).

Frequently Asked Questions (FAQs)

Frequently asked questions

Q: Is this policy supported in Infracost?

A: Yes. This policy is available in Infracost, including in the free trial. Infracost detects Azure SQL vCore configurations and calculates their cost impact automatically within your Terraform workflow.

Q: What vCore count should non-production Azure SQL databases use?

A: Most non-production workloads operate effectively with 2 vCores. If your workload requires more during testing phases, consider using 4 vCores as a maximum and requiring justification for any higher allocation.

Q: Can this policy be enforced automatically in CI/CD pipelines?

A: Yes. Infracost integrates into CI/CD pipelines and can flag or block pull requests where non-production Azure SQL databases exceed a defined vCore threshold. Azure Policy can enforce the same restriction at the infrastructure level.

Q: Does switching to Serverless cause application downtime?

A: Migrating an existing Azure SQL database to the Serverless compute tier requires a service tier change, which may involve a brief connection interruption. Plan this change during a maintenance window and verify connectivity after the change is applied.

Q: How does Infracost help teams track progress on this policy over time?

A: Infracost provides cost reporting that tracks changes across pull requests and over time. FinOps teams can measure how many non-production databases have been right-sized, monitor remaining violations, and quantify cumulative savings as the policy is applied across the estate.

Q: Does this policy apply to Azure SQL Managed Instance?

A: Yes. Azure SQL Managed Instance also uses a vCore-based model. The same principle applies: non-production managed instances should use the minimum vCore count that meets functional requirements, typically 4 or 8 vCores rather than the 16-80 vCore configurations common in production.

Q: What is the difference between reducing vCores and changing the service tier?

A: Reducing vCores within the same service tier (e.g., from General Purpose 8 to General Purpose 2) scales compute cost directly. Changing the service tier (e.g., from Business Critical to General Purpose) additionally removes premium features such as in-memory OLTP and higher IOPS. For most non-production workloads, both changes are appropriate and compound the savings.

Create Free Account

This policy is supported in Infracost and available in the free trial. Sign up today and scan your code using our entire library of FinOps policies.