Back to Blog
5 min read

Reserved Capacity Planning for Azure

Reserved capacity can save 20-72% on Azure costs, but requires careful planning. Let’s explore how to make smart reservation decisions.

Understanding Reserved Capacity

Azure offers reservations for:

  • Virtual Machines
  • Azure SQL Database
  • Cosmos DB
  • Storage
  • Azure Synapse Analytics
  • App Service
  • Azure Databricks
  • Azure Cache for Redis

Commitment Options

TermTypical SavingsBest For
1 Year20-40%Moderate confidence in usage
3 Year40-72%Stable, predictable workloads

Analysis Framework

from dataclasses import dataclass
from typing import List
import pandas as pd

@dataclass
class ResourceUsage:
    resource_type: str
    sku: str
    region: str
    monthly_hours: float
    monthly_cost: float

@dataclass
class ReservationRecommendation:
    resource_type: str
    sku: str
    region: str
    quantity: int
    term_years: int
    monthly_savings: float
    total_savings: float
    break_even_months: int
    confidence: str

class ReservationAnalyzer:
    # Approximate savings percentages
    SAVINGS = {
        "VirtualMachines": {"1yr": 0.35, "3yr": 0.55},
        "SqlDatabase": {"1yr": 0.30, "3yr": 0.50},
        "CosmosDB": {"1yr": 0.25, "3yr": 0.45},
    }

    def analyze(self, usage_data: List[ResourceUsage]) -> List[ReservationRecommendation]:
        """Analyze usage and generate reservation recommendations."""

        recommendations = []

        # Group by resource type, SKU, and region
        df = pd.DataFrame([vars(u) for u in usage_data])
        grouped = df.groupby(['resource_type', 'sku', 'region']).agg({
            'monthly_hours': 'mean',
            'monthly_cost': 'mean'
        }).reset_index()

        for _, row in grouped.iterrows():
            # Calculate uptime percentage
            max_hours = 730  # Hours in a month
            uptime_pct = row['monthly_hours'] / max_hours

            if uptime_pct < 0.5:
                continue  # Not a good candidate

            # Determine recommended term
            if uptime_pct > 0.9:
                term = 3
                confidence = "High"
            elif uptime_pct > 0.7:
                term = 1
                confidence = "Medium"
            else:
                term = 1
                confidence = "Low"

            # Calculate savings
            savings_key = f"{term}yr"
            savings_pct = self.SAVINGS.get(row['resource_type'], {}).get(savings_key, 0.20)

            monthly_savings = row['monthly_cost'] * savings_pct
            total_savings = monthly_savings * (term * 12)

            # Calculate break-even (accounting for upfront cost)
            upfront_cost = row['monthly_cost'] * (1 - savings_pct) * term * 12
            break_even = int(upfront_cost / monthly_savings) if monthly_savings > 0 else 999

            recommendations.append(ReservationRecommendation(
                resource_type=row['resource_type'],
                sku=row['sku'],
                region=row['region'],
                quantity=1,
                term_years=term,
                monthly_savings=monthly_savings,
                total_savings=total_savings,
                break_even_months=break_even,
                confidence=confidence
            ))

        return sorted(recommendations, key=lambda x: x.total_savings, reverse=True)

VM Reservation Strategy

def vm_reservation_strategy(vm_inventory: List[dict]) -> dict:
    """Create VM reservation purchase strategy."""

    strategy = {
        "immediate_purchase": [],
        "consider_carefully": [],
        "not_recommended": [],
        "total_monthly_savings": 0
    }

    for vm in vm_inventory:
        uptime = vm["uptime_percentage"]
        stability = vm["deployment_stability"]  # How long it's been running
        growth_trend = vm["growth_trend"]  # Increasing/stable/decreasing

        if uptime > 90 and stability == "stable" and growth_trend != "decreasing":
            # Strong candidate for 3-year reservation
            savings = calculate_savings(vm, term=3)
            strategy["immediate_purchase"].append({
                "vm": vm["name"],
                "sku": vm["sku"],
                "region": vm["region"],
                "term": "3 years",
                "monthly_savings": savings
            })
            strategy["total_monthly_savings"] += savings

        elif uptime > 70:
            # Consider 1-year reservation
            savings = calculate_savings(vm, term=1)
            strategy["consider_carefully"].append({
                "vm": vm["name"],
                "sku": vm["sku"],
                "reason": f"Uptime: {uptime}%, Trend: {growth_trend}",
                "potential_savings": savings
            })

        else:
            strategy["not_recommended"].append({
                "vm": vm["name"],
                "reason": f"Low uptime ({uptime}%) - use pay-as-you-go"
            })

    return strategy

Database Reservation Considerations

# Azure SQL Database reservation analysis
def analyze_sql_reservations(databases: List[dict]) -> dict:
    """Analyze Azure SQL databases for reservation opportunities."""

    recommendations = {
        "vcore_reservations": [],
        "dtu_to_vcore_migration": [],
        "elastic_pool_opportunities": []
    }

    # Group by vCore count
    vcore_groups = {}
    for db in databases:
        if db["pricing_model"] == "vCore":
            key = (db["vcore_count"], db["region"])
            vcore_groups.setdefault(key, []).append(db)

    for (vcores, region), dbs in vcore_groups.items():
        total_usage_hours = sum(db["monthly_hours"] for db in dbs)
        if total_usage_hours > 700 * len(dbs):  # >95% uptime average
            recommendations["vcore_reservations"].append({
                "vcores": vcores,
                "region": region,
                "database_count": len(dbs),
                "recommendation": "3-year reservation",
                "estimated_savings": "~55%"
            })

    return recommendations

Cosmos DB Reserved Capacity

def cosmos_db_reservation_analysis(containers: List[dict]) -> dict:
    """Analyze Cosmos DB for reserved throughput."""

    analysis = {
        "reserved_throughput_candidates": [],
        "autoscale_recommendations": [],
        "serverless_candidates": []
    }

    for container in containers:
        avg_rus = container["avg_ru_usage"]
        peak_rus = container["peak_ru_usage"]
        provisioned_rus = container["provisioned_ru"]

        utilization = avg_rus / provisioned_rus if provisioned_rus > 0 else 0

        if utilization > 0.7:
            # Good candidate for reserved throughput
            analysis["reserved_throughput_candidates"].append({
                "container": container["name"],
                "current_rus": provisioned_rus,
                "recommended_reserved": int(avg_rus * 1.2),  # 20% buffer
                "savings": "~25% (1yr) or ~45% (3yr)"
            })

        elif utilization < 0.3 and peak_rus > avg_rus * 3:
            # Highly variable - consider autoscale
            analysis["autoscale_recommendations"].append({
                "container": container["name"],
                "current_rus": provisioned_rus,
                "recommended": f"Autoscale {int(avg_rus)} - {int(peak_rus)}"
            })

        elif avg_rus < 1000:
            # Low usage - consider serverless
            analysis["serverless_candidates"].append({
                "container": container["name"],
                "avg_rus": avg_rus,
                "note": "Evaluate serverless option"
            })

    return analysis

Reservation Management

Flexibility Options

reservation_flexibility:
  instance_size_flexibility:
    description: VMs in same series can share reservation
    example:
      - D2s_v3 reservation covers 1x D2s_v3
      - Same reservation covers 0.5x D4s_v3
      - Or 0.25x D8s_v3
    recommendation: Enable for all VM reservations

  scope_options:
    single_subscription:
      use_when: Clear cost allocation needed
      flexibility: Limited

    shared_scope:
      use_when: Multiple subscriptions, maximize utilization
      flexibility: High
      recommendation: Default choice for most organizations

    resource_group:
      use_when: Specific workload isolation
      flexibility: Limited

Exchange and Refund Policies

reservation_policies = {
    "exchange": {
        "allowed": True,
        "fee": "None",
        "restrictions": [
            "Must exchange for same type (VM for VM)",
            "New reservation must be equal or greater value",
            "Regional restrictions may apply"
        ]
    },
    "refund": {
        "allowed": True,
        "fee": "12% early termination fee",
        "limit": "$50,000 per rolling 12 months",
        "note": "Consider exchange instead of refund when possible"
    }
}

Planning Calendar

reservation_planning_calendar:
  january:
    - Analyze previous year's usage
    - Identify stable workloads
    - Plan Q1 purchases

  quarterly:
    - Review utilization of existing reservations
    - Assess new workloads for eligibility
    - Exchange underutilized reservations

  ongoing:
    - Monitor utilization dashboards
    - Track new Azure reservation offerings
    - Update forecasts based on business changes

Conclusion

Reserved capacity offers significant savings but requires careful analysis. Focus on stable, predictable workloads with high utilization. Start with 1-year terms to build confidence, then move to 3-year terms for proven stable resources. Always maintain some pay-as-you-go capacity for flexibility.

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.