Back to Blog
5 min read

Microsoft Fabric Licensing and Capacity Planning Guide

Microsoft Fabric Licensing and Capacity Planning Guide

With Fabric now GA, understanding the licensing model is crucial for planning your deployment. Let’s break down the options and help you right-size your capacity.

Fabric Licensing Options

1. Fabric Capacity (Pay-as-you-go)

Azure-based consumption model with Capacity Units (CUs):

SKUCUsMemorySpark vCoresIdeal For
F223 GB2Dev/Test
F446 GB4Small teams
F8812 GB8Department
F161624 GB16Medium workloads
F323248 GB32Large workloads
F646496 GB64Enterprise
F128+128+192+ GB128+Large enterprise

2. Power BI Premium per Capacity

Existing P SKUs now include Fabric workloads:

# Capacity SKU mapping
capacity_mapping = {
    "P1": {"cus": 8, "fabric_sku": "F8"},
    "P2": {"cus": 16, "fabric_sku": "F16"},
    "P3": {"cus": 32, "fabric_sku": "F32"},
    "P4": {"cus": 64, "fabric_sku": "F64"},
    "P5": {"cus": 128, "fabric_sku": "F128"}
}

def get_equivalent_fabric_sku(pbi_sku: str) -> dict:
    return capacity_mapping.get(pbi_sku, {"error": "Unknown SKU"})

3. Fabric Trial

60-day trial with F64 equivalent capacity - perfect for evaluation.

Capacity Planning Framework

Step 1: Assess Current Workloads

from dataclasses import dataclass
from typing import List
import json

@dataclass
class Workload:
    name: str
    type: str  # 'spark', 'warehouse', 'realtime', 'powerbi'
    daily_runs: int
    avg_duration_minutes: float
    peak_concurrent_users: int
    data_volume_gb: float

def assess_workloads(workloads: List[Workload]) -> dict:
    """Assess total capacity needs from workloads."""
    total_compute_hours = sum(
        w.daily_runs * w.avg_duration_minutes / 60
        for w in workloads
    )

    peak_users = max(w.peak_concurrent_users for w in workloads)
    total_storage = sum(w.data_volume_gb for w in workloads)

    return {
        "daily_compute_hours": total_compute_hours,
        "peak_concurrent_users": peak_users,
        "total_storage_gb": total_storage,
        "workload_breakdown": {
            w_type: sum(
                w.daily_runs * w.avg_duration_minutes
                for w in workloads if w.type == w_type
            )
            for w_type in set(w.type for w in workloads)
        }
    }

# Example assessment
workloads = [
    Workload("ETL Pipeline", "spark", 24, 30, 1, 500),
    Workload("Reports Refresh", "powerbi", 48, 5, 50, 100),
    Workload("Ad-hoc Queries", "warehouse", 100, 2, 20, 200),
    Workload("Stream Processing", "realtime", 1, 1440, 5, 50)
]

assessment = assess_workloads(workloads)
print(json.dumps(assessment, indent=2))

Step 2: Calculate Required Capacity

def recommend_capacity(assessment: dict, growth_factor: float = 1.3) -> dict:
    """Recommend Fabric capacity based on assessment."""
    compute_hours = assessment["daily_compute_hours"]
    peak_users = assessment["peak_concurrent_users"]

    # Base calculation
    base_cus = 0

    # Spark workloads: ~2 CUs per concurrent Spark job
    spark_cus = assessment["workload_breakdown"].get("spark", 0) / 60 * 2

    # Warehouse: ~1 CU per 10 concurrent queries
    warehouse_cus = peak_users / 10

    # Power BI: ~4 CUs per 100 concurrent viewers
    powerbi_cus = peak_users / 100 * 4

    # Real-time: ~8 CUs minimum for always-on processing
    realtime_cus = 8 if assessment["workload_breakdown"].get("realtime", 0) > 0 else 0

    total_cus = (spark_cus + warehouse_cus + powerbi_cus + realtime_cus) * growth_factor

    # Map to SKU
    skus = [
        ("F2", 2), ("F4", 4), ("F8", 8), ("F16", 16),
        ("F32", 32), ("F64", 64), ("F128", 128), ("F256", 256)
    ]

    recommended_sku = "F256"  # default to largest
    for sku_name, sku_cus in skus:
        if sku_cus >= total_cus:
            recommended_sku = sku_name
            break

    return {
        "calculated_cus": round(total_cus, 1),
        "recommended_sku": recommended_sku,
        "breakdown": {
            "spark": round(spark_cus, 1),
            "warehouse": round(warehouse_cus, 1),
            "powerbi": round(powerbi_cus, 1),
            "realtime": round(realtime_cus, 1)
        },
        "growth_factor_applied": growth_factor
    }

recommendation = recommend_capacity(assessment)
print(json.dumps(recommendation, indent=2))

Step 3: Cost Estimation

# Azure pricing (approximate, check current prices)
FABRIC_PRICING = {
    "F2": {"hourly": 0.36, "monthly_reserved": 180},
    "F4": {"hourly": 0.72, "monthly_reserved": 360},
    "F8": {"hourly": 1.44, "monthly_reserved": 720},
    "F16": {"hourly": 2.88, "monthly_reserved": 1440},
    "F32": {"hourly": 5.76, "monthly_reserved": 2880},
    "F64": {"hourly": 11.52, "monthly_reserved": 5760},
    "F128": {"hourly": 23.04, "monthly_reserved": 11520}
}

def estimate_costs(sku: str, usage_pattern: str = "always_on") -> dict:
    """Estimate monthly costs for a Fabric SKU."""
    if sku not in FABRIC_PRICING:
        return {"error": "Unknown SKU"}

    pricing = FABRIC_PRICING[sku]

    if usage_pattern == "always_on":
        payg_cost = pricing["hourly"] * 24 * 30
        reserved_cost = pricing["monthly_reserved"]
        savings = payg_cost - reserved_cost
    elif usage_pattern == "business_hours":  # 10 hours/day, weekdays
        payg_cost = pricing["hourly"] * 10 * 22
        reserved_cost = pricing["monthly_reserved"]  # Still need capacity
        savings = payg_cost - reserved_cost if payg_cost > reserved_cost else 0
    else:  # burst
        payg_cost = pricing["hourly"] * 8 * 30  # 8 hours average
        reserved_cost = pricing["monthly_reserved"]
        savings = 0

    return {
        "sku": sku,
        "pattern": usage_pattern,
        "payg_monthly": round(payg_cost, 2),
        "reserved_monthly": reserved_cost,
        "savings_with_reserved": round(savings, 2),
        "recommendation": "reserved" if savings > 0 else "payg"
    }

# Compare costs
for pattern in ["always_on", "business_hours", "burst"]:
    estimate = estimate_costs("F32", pattern)
    print(f"{pattern}: ${estimate['payg_monthly']} PAYG vs ${estimate['reserved_monthly']} Reserved")

Autoscale and Burst

Fabric supports smoothing and burst capabilities:

# Capacity settings configuration
capacity_config = {
    "sku": "F32",
    "smoothing": {
        "enabled": True,
        "window_minutes": 5  # Average usage over 5-minute windows
    },
    "burst": {
        "enabled": True,
        "max_burst_factor": 1.5,  # Can burst to 150% of capacity
        "accumulated_seconds": 86400  # 24 hours of burst credits
    },
    "auto_pause": {
        "enabled": False,  # Only for dev/test
        "idle_minutes": 30
    }
}

def calculate_burst_availability(
    base_capacity: int,
    burst_factor: float,
    usage_history: List[float]
) -> dict:
    """Calculate available burst capacity."""
    avg_usage = sum(usage_history) / len(usage_history)
    underutilization = max(0, base_capacity - avg_usage)

    # Burst credits accumulate when under capacity
    burst_credits = underutilization * len(usage_history)
    max_burst = base_capacity * burst_factor

    return {
        "base_capacity": base_capacity,
        "average_usage": round(avg_usage, 1),
        "burst_credits_available": round(burst_credits, 1),
        "max_burst_capacity": max_burst,
        "can_burst": burst_credits > 0
    }

Best Practices

  1. Start small, scale up - Begin with F8 for most workloads
  2. Use reserved capacity for predictable workloads
  3. Enable smoothing to handle usage spikes
  4. Monitor capacity metrics in the Fabric admin portal
  5. Consider multi-region for global deployments

Tomorrow, we’ll explore Fabric Governance and how to manage your data platform securely.

Michael John Peña

Michael John Peña

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